karaf-issues mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From "ASF GitHub Bot (JIRA)" <j...@apache.org>
Subject [jira] [Commented] (KARAF-5108) Add deployer feature
Date Wed, 22 Nov 2017 10:51:00 GMT

    [ https://issues.apache.org/jira/browse/KARAF-5108?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=16262270#comment-16262270 ] 

ASF GitHub Bot commented on KARAF-5108:
---------------------------------------

jbonofre closed pull request #7: [KARAF-5108] Add Deployer feature
URL: https://github.com/apache/karaf-cave/pull/7
 
 
   

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/assembly/src/main/resources/features.xml b/assembly/src/main/resources/features.xml
index 978f59f..81754b4 100644
--- a/assembly/src/main/resources/features.xml
+++ b/assembly/src/main/resources/features.xml
@@ -66,4 +66,29 @@
         <bundle>mvn:org.apache.karaf.cave.server/org.apache.karaf.cave.server.maven/${project.version}</bundle>
     </feature>
 
+    <feature name="cave-deployer" version="${project.version}">
+        <feature version="${project.version}">cave-deployer-rest</feature>
+    </feature>
+
+    <feature name="cave-deployer-service" version="${project.version}">
+        <bundle>mvn:org.apache.karaf.cave.deployer/org.apache.karaf.cave.deployer.api/${project.version}</bundle>
+        <bundle dependency="true">mvn:org.apache.httpcomponents/httpcore-osgi/4.2.5</bundle>
+        <bundle dependency="true">mvn:org.apache.httpcomponents/httpclient-osgi/4.2.5</bundle>
+        <bundle dependency="true">mvn:com.google.inject/guice/3.0</bundle>
+        <bundle dependency="true">mvn:com.google.guava/guava/18.0</bundle>
+        <bundle>mvn:org.apache.karaf.cave.deployer/org.apache.karaf.cave.deployer.service/${project.version}</bundle>
+    </feature>
+
+    <feature name="cave-deployer-rest" version="${project.version}">
+        <feature version="${cxf.version}">cxf-jaxrs</feature>
+        <feature version="${project.version}">cave-deployer-service</feature>
+        <bundle dependency="true">mvn:com.fasterxml.jackson.core/jackson-core/2.4.6</bundle>
+        <bundle dependency="true">mvn:com.fasterxml.jackson.core/jackson-annotations/2.4.6</bundle>
+        <bundle dependency="true">mvn:com.fasterxml.jackson.core/jackson-databind/2.4.6</bundle>
+        <bundle dependency="true">mvn:com.fasterxml.jackson.jaxrs/jackson-jaxrs-base/2.4.6</bundle>
+        <bundle dependency="true">mvn:com.fasterxml.jackson.jaxrs/jackson-jaxrs-json-provider/2.4.6</bundle>
+        <bundle>mvn:org.apache.karaf.cave.deployer/org.apache.karaf.cave.deployer.rest/${project.version}</bundle>
+    </feature>
+
+
 </features>
\ No newline at end of file
diff --git a/deployer/api/pom.xml b/deployer/api/pom.xml
new file mode 100644
index 0000000..81fe7a7
--- /dev/null
+++ b/deployer/api/pom.xml
@@ -0,0 +1,54 @@
+<?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">
+
+    <!--
+
+        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.
+    -->
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.karaf.cave</groupId>
+        <artifactId>org.apache.karaf.cave.deployer</artifactId>
+        <version>4.1.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <groupId>org.apache.karaf.cave.deployer</groupId>
+    <artifactId>org.apache.karaf.cave.deployer.api</artifactId>
+    <name>Apache Karaf :: Cave :: Deployer :: API</name>
+    <packaging>bundle</packaging>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <inherited>true</inherited>
+                <configuration>
+                    <instructions>
+                        <Export-Package>
+                            org.apache.karaf.cave.deployer.api
+                        </Export-Package>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
\ No newline at end of file
diff --git a/deployer/api/src/main/java/org/apache/karaf/cave/deployer/api/Bundle.java b/deployer/api/src/main/java/org/apache/karaf/cave/deployer/api/Bundle.java
new file mode 100644
index 0000000..8c1ea92
--- /dev/null
+++ b/deployer/api/src/main/java/org/apache/karaf/cave/deployer/api/Bundle.java
@@ -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 org.apache.karaf.cave.deployer.api;
+
+/**
+ * Simplified wrapper representing a bundle.
+ */
+public class Bundle {
+
+    private String id;
+    private String name;
+    private String version;
+    private String state;
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getVersion() {
+        return version;
+    }
+
+    public void setVersion(String version) {
+        this.version = version;
+    }
+
+    public String getState() {
+        return state;
+    }
+
+    public void setState(String state) {
+        this.state = state;
+    }
+}
diff --git a/deployer/api/src/main/java/org/apache/karaf/cave/deployer/api/Config.java b/deployer/api/src/main/java/org/apache/karaf/cave/deployer/api/Config.java
new file mode 100644
index 0000000..ea7cff8
--- /dev/null
+++ b/deployer/api/src/main/java/org/apache/karaf/cave/deployer/api/Config.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.karaf.cave.deployer.api;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Simplified wrapper representing a config.
+ */
+public class Config {
+
+    private String pid;
+    private Map<String, String> properties;
+
+    public Config() {
+        properties = new HashMap<>();
+    }
+
+    public String getPid() {
+        return pid;
+    }
+
+    public void setPid(String pid) {
+        this.pid = pid;
+    }
+
+    public Map<String, String> getProperties() {
+        return properties;
+    }
+
+    public void setProperties(Map<String, String> properties) {
+        this.properties = properties;
+    }
+
+}
diff --git a/deployer/api/src/main/java/org/apache/karaf/cave/deployer/api/Deployer.java b/deployer/api/src/main/java/org/apache/karaf/cave/deployer/api/Deployer.java
new file mode 100644
index 0000000..de1f1a7
--- /dev/null
+++ b/deployer/api/src/main/java/org/apache/karaf/cave/deployer/api/Deployer.java
@@ -0,0 +1,490 @@
+/*
+ * 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.karaf.cave.deployer.api;
+
+import java.util.List;
+import java.util.Map;
+
+public interface Deployer {
+
+    /**
+     * Explode a KAR file and upload the content on a Maven repository.
+     *
+     * @param karUrl The location of the KAR file.
+     * @param repositoryUrl The location of the Maven repository where to upload.
+     * @throws Exception in case of failure.
+     */
+    void explodeKar(String karUrl, String repositoryUrl) throws Exception;
+
+    /**
+     * Download an artifact from a given URL and copy it on a local filesystem.
+     *
+     * @param artifactUrl The artifact URL.
+     * @param localUrl The local filesystem URL.
+     * @throws Exception in case of failure.
+     */
+    void downloadArtifact(String artifactUrl, String localUrl) throws Exception;
+
+    /**
+     * Upload an artifact to a Maven repository using the provided Maven coordinates.
+     *
+     * @param groupId The resulting artifact groupId.
+     * @param artifactId The resulting artifact artifactId.
+     * @param version The resulting artifact version.
+     * @param artifactUrl The source artifact location.
+     * @param repositoryUrl The location of the Maven repository where to upload.
+     * @throws Exception in case of failure.
+     */
+    void uploadArtifact(String groupId, String artifactId, String version, String artifactUrl, String repositoryUrl)
+        throws Exception;
+
+    /**
+     * Create/assemble a feature based on existing ones. It allows you to create "meta" feature.
+     *
+     * @param groupId The resulting features repository groupId.
+     * @param artifactId The resulting features repository artifactId.
+     * @param version The resulting features repository version.
+     * @param repositoryUrl The location of the Maven repository where to upload the features repository.
+     * @param feature The name of the resulting feature.
+     * @param featureRepositoryUrls The source features repository URLs.
+     * @param features The source feature names.
+     * @param bundles The source bundle locations.
+     * @param configs The source configuration PIDs.
+     * @throws Exception in case of failure.
+     */
+    void assembleFeature(String groupId, String artifactId, String version, String repositoryUrl, String feature,
+                         List<String> featureRepositoryUrls,
+                         List<String> features,
+                         List<String> bundles,
+                         List<Config> configs) throws Exception;
+
+    /**
+     * A simple remote deployment operation for bundle. You can deploy a bundle to a remote Karaf instance.
+     *
+     * @param artifactUrl The location of the bundle to deploy.
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @throws Exception in case of failure.
+     */
+    void deployBundle(String artifactUrl, String jmxUrl, String karafName, String user, String password)
+        throws Exception;
+
+    /**
+     * A simple remote undeploy operation for bundle. You can undeploy a bundle from a remote Karaf instance.
+     *
+     * @param id The bundle ID to undeploy.
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @throws Exception in case of failure.
+     */
+    void undeployBundle(String id, String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * A simple remote start operation for bundle. You can start a bundle on a remote Karaf instance.
+     *
+     * @param id The bundle ID to start.
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @throws Exception in case of failure.
+     */
+    void startBundle(String id, String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * A simple remote stop operation for bundle. You can stop a bundle on a remote Karaf instance.
+     *
+     * @param id The bundle ID to stop.
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @throws Exception in case of failure.
+     */
+    void stopBundle(String id, String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation to list bundles on a remote Karaf instance.
+     *
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user Te username for the MBean server.
+     * @param password The password for the MBean server.
+     * @return The list of bundles.
+     * @throws Exception in case of failure.
+     */
+    List<Bundle> listBundles(String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation to install a KAR on a remote Karaf instance.
+     *
+     * @param karUrl The location of the KAR file.
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @throws Exception in case of failure.
+     */
+    void installKar(String karUrl, String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation to uninstall a KAR from a remote Karaf instance.
+     *
+     * @param id The name/id of the deployed KAR.
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @throws Exception in case of failure.
+     */
+    void uninstallKar(String id, String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation to list the KAR on a remote Karaf instance.
+     *
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @return The list of KAR.
+     * @throws Exception in case of failure.
+     */
+    List<String> listKars(String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation to add a features repository to a remote Karaf instance.
+     *
+     * @param featuresRepositoryUrl The location of the features repository to add.
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @throws Exception in case of failure.
+     */
+    void addFeaturesRepository(String featuresRepositoryUrl, String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation to remove a features repository from a remote Karaf instance.
+     *
+     * @param featuresRepositoryUrl The location of the features repository to remove.
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @throws Exception in case of failure.
+     */
+    void removeFeaturesRepository(String featuresRepositoryUrl, String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation listing the features repository on a remote Karaf instance.
+     *
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @return The list of features repositories.
+     * @throws Exception in case of failure.
+     */
+    List<FeaturesRepository> listFeaturesRepositories(String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation to install a feature on a remote Karaf instance.
+     *
+     * @param feature The feature to install.
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @throws Exception in case of failure.
+     */
+    void installFeature(String feature, String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation to uninstall a feature from a remote Karaf instance.
+     *
+     * @param feature The feature to uninstall.
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @throws Exception in case of failure.
+     */
+    void uninstallFeature(String feature, String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation to list features available on a remote Karaf instance.
+     *
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @return The list of features.
+     * @throws Exception in case of failure.
+     */
+    List<Feature> listFeatures(String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation to list features installed on a remote Karaf instance.
+     *
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @return The list of features.
+     * @throws Exception in case of failure.
+     */
+    List<String> listInstalledFeatures(String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation to create an empty configuration on a remote Karaf instance.
+     *
+     * @param pid The configuration PID.
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @throws Exception in case of failure.
+     */
+    void createConfig(String pid, String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation to get the properties of a given configuration on a remote Karaf instance.
+     *
+     * @param pid The configuration PID.
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @return The configuration properties.
+     * @throws Exception in case of failure.
+     */
+    Map<String, String> getConfigProperties(String pid, String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation to update a configuration in a remote Karaf instance.
+     *
+     * @param config The configuration.
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @throws Exception in case of failure.
+     */
+    void updateConfig(Config config, String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation to delete a configuration from a remote Karaf instance.
+     *
+     * @param pid The configuration PID.
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @throws Exception in case of failure.
+     */
+    void deleteConfig(String pid, String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation to append a value of the end of the current one for a config property on a remote Karaf instance.
+     *
+     * @param pid The configuration PID.
+     * @param key The configuration property key.
+     * @param value The value to append at the end of the current property value.
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @throws Exception in case of failure.
+     */
+    void appendConfigProperty(String pid, String key, String value, String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation to set a value of a config property on a remote Karaf instance.
+     *
+     * @param pid The configuration PID.
+     * @param key The configuration property key.
+     * @param value The new configuration property value.
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @throws Exception in case of failure.
+     */
+    void setConfigProperty(String pid, String key, String value, String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation to get the value of a config property on a remote Karaf instance.
+     *
+     * @param pid The configuration PID.
+     * @param key The configuration property key.
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @return The current config property value.
+     * @throws Exception in case of failure.
+     */
+    String getConfigProperty(String pid, String key, String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation to delete a config property from a remote Karaf instance.
+     *
+     * @param pid The configuration PID.
+     * @param key The configuration property key.
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @throws Exception in case of failure.
+     */
+    void deleteConfigProperty(String pid, String key, String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation listing the Cellar cluster nodes if available.
+     *
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @return The list of Cellar cluster nodes if available.
+     * @throws Exception in case of failure.
+     */
+    List<String> clusterNodes(String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation getting the Cellar cluster groups if available.
+     *
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @return The list of Cellar cluster groups if available.
+     * @throws Exception in case of failure.
+     */
+    Map<String, List<String>> clusterGroups(String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation to add a features repository to a Cellar cluster group if available.
+     *
+     * @param repositoryId The features repository URL or ID.
+     * @param clusterGroup The Cellar cluster group.
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @throws Exception in case of failure.
+     */
+    void clusterAddFeaturesRepository(String repositoryId, String clusterGroup, String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation to remove a features repository from a Cellar cluster group if available.
+     *
+     * @param repositoryId The features repository URL or ID.
+     * @param clusterGroup The Cellar cluster group.
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @throws Exception in case of failure.
+     */
+    void clusterRemoveFeaturesRepository(String repositoryId, String clusterGroup, String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation to check if a features repository is present on a given Cellar cluster group.
+     *
+     * @param repositoryId The features repository URL or ID.
+     * @param clusterGroup The Cellar cluster group.
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @return True if the features repository is present in the Cellar cluster group, false else.
+     * @throws Exception in case of failure.
+     */
+    boolean isFeaturesRepositoryOnClusterGroup(String repositoryId, String clusterGroup, String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation to check if a features repository is present on a remote Karaf instance.
+     *
+     * @param repositoryId The features repository URL or ID.
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @return True if the features repository is present on the Karaf instance, false else.
+     * @throws Exception in case of failure.
+     */
+    boolean isFeaturesRepositoryLocal(String repositoryId, String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation to install a feature on a Cellar cluster group.
+     *
+     * @param feature The feature to install on the Cellar cluster group.
+     * @param clusterGroup The Cellar cluster group.
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @throws Exception in case of failure.
+     */
+    void clusterFeatureInstall(String feature, String clusterGroup, String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation to uninstall a feature on a Cellar cluster group.
+     *
+     * @param feature The feature to uninstall from the Cellar cluster group.
+     * @param clusterGroup The Cellar cluster group.
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @throws Exception in case of failure.
+     */
+    void clusterFeatureUninstall(String feature, String clusterGroup, String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation to check if a feature is present on a given Cellar cluster group.
+     *
+     * @param feature The feature.
+     * @param clusterGroup The Cellar cluster group.
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @return True if the feature is present in the Cellar cluster group, false else.
+     * @throws Exception in case of failure.
+     */
+    boolean isFeatureOnClusterGroup(String feature, String clusterGroup, String jmxUrl, String karafName, String user, String password) throws Exception;
+
+    /**
+     * Simple remote operation to check if a feature is present on a given Karaf instance.
+     *
+     * @param feature The feature.
+     * @param jmxUrl The JMX location of the target Karaf instance.
+     * @param karafName The name of the Karaf instance.
+     * @param user The username for the MBean server.
+     * @param password The password for the MBean server.
+     * @return True if the feature is present, false else.
+     * @throws Exception in case of failure.
+     */
+    boolean isFeatureLocal(String feature, String jmxUrl, String karafName, String user, String password) throws Exception;
+
+}
diff --git a/deployer/api/src/main/java/org/apache/karaf/cave/deployer/api/Feature.java b/deployer/api/src/main/java/org/apache/karaf/cave/deployer/api/Feature.java
new file mode 100644
index 0000000..da1c034
--- /dev/null
+++ b/deployer/api/src/main/java/org/apache/karaf/cave/deployer/api/Feature.java
@@ -0,0 +1,52 @@
+/*
+ * 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.karaf.cave.deployer.api;
+
+/**
+ * Simplified wrapper representing a feature.
+ */
+public class Feature {
+
+    private String name;
+    private String version;
+    private String state;
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getVersion() {
+        return version;
+    }
+
+    public void setVersion(String version) {
+        this.version = version;
+    }
+
+    public String getState() {
+        return state;
+    }
+
+    public void setState(String state) {
+        this.state = state;
+    }
+
+}
diff --git a/deployer/api/src/main/java/org/apache/karaf/cave/deployer/api/FeaturesRepository.java b/deployer/api/src/main/java/org/apache/karaf/cave/deployer/api/FeaturesRepository.java
new file mode 100644
index 0000000..1bf704f
--- /dev/null
+++ b/deployer/api/src/main/java/org/apache/karaf/cave/deployer/api/FeaturesRepository.java
@@ -0,0 +1,43 @@
+/*
+ * 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.karaf.cave.deployer.api;
+
+/**
+ * Simplified wrapper representing a features repository.
+ */
+public class FeaturesRepository {
+
+    private String name;
+    private String uri;
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getUri() {
+        return uri;
+    }
+
+    public void setUri(String uri) {
+        this.uri = uri;
+    }
+
+}
diff --git a/deployer/pom.xml b/deployer/pom.xml
new file mode 100644
index 0000000..500bd50
--- /dev/null
+++ b/deployer/pom.xml
@@ -0,0 +1,42 @@
+<?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">
+
+    <!--
+
+        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.
+    -->
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.karaf</groupId>
+        <artifactId>cave</artifactId>
+        <version>4.1.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <groupId>org.apache.karaf.cave</groupId>
+    <artifactId>org.apache.karaf.cave.deployer</artifactId>
+    <name>Apache Karaf :: Cave :: Deployer</name>
+    <packaging>pom</packaging>
+
+    <modules>
+        <module>api</module>
+        <module>service</module>
+        <module>rest</module>
+    </modules>
+
+</project>
\ No newline at end of file
diff --git a/deployer/rest/pom.xml b/deployer/rest/pom.xml
new file mode 100644
index 0000000..9322888
--- /dev/null
+++ b/deployer/rest/pom.xml
@@ -0,0 +1,86 @@
+<?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">
+
+    <!--
+
+        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.
+    -->
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.karaf.cave</groupId>
+        <artifactId>org.apache.karaf.cave.deployer</artifactId>
+        <version>4.1.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <groupId>org.apache.karaf.cave.deployer</groupId>
+    <artifactId>org.apache.karaf.cave.deployer.rest</artifactId>
+    <name>Apache Karaf :: Cave :: Deployer :: REST</name>
+    <packaging>bundle</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.karaf.cave.deployer</groupId>
+            <artifactId>org.apache.karaf.cave.deployer.api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.cxf</groupId>
+            <artifactId>cxf-rt-frontend-jaxrs</artifactId>
+            <version>${cxf.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.cxf</groupId>
+            <artifactId>cxf-rt-transports-http</artifactId>
+            <version>${cxf.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.12</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.karaf.cave.deployer</groupId>
+            <artifactId>org.apache.karaf.cave.deployer.service</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <inherited>true</inherited>
+                <configuration>
+                    <instructions>
+                        <Import-Package>
+                            org.apache.karaf.cave.deploy.api,
+                            *
+                        </Import-Package>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
\ No newline at end of file
diff --git a/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/BasicRequest.java b/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/BasicRequest.java
new file mode 100644
index 0000000..0af6f0e
--- /dev/null
+++ b/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/BasicRequest.java
@@ -0,0 +1,58 @@
+/*
+ * 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.karaf.cave.deployer.rest;
+
+public class BasicRequest {
+
+    private String jmxUrl;
+    private String karafName;
+    private String user;
+    private String password;
+
+    public String getJmxUrl() {
+        return jmxUrl;
+    }
+
+    public void setJmxUrl(String jmxUrl) {
+        this.jmxUrl = jmxUrl;
+    }
+
+    public String getKarafName() {
+        return karafName;
+    }
+
+    public void setKarafName(String karafName) {
+        this.karafName = karafName;
+    }
+
+    public String getUser() {
+        return user;
+    }
+
+    public void setUser(String user) {
+        this.user = user;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+}
diff --git a/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/ClusterDeployRequest.java b/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/ClusterDeployRequest.java
new file mode 100644
index 0000000..6eb4aff
--- /dev/null
+++ b/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/ClusterDeployRequest.java
@@ -0,0 +1,31 @@
+/*
+ * 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.karaf.cave.deployer.rest;
+
+public class ClusterDeployRequest extends DeployRequest {
+
+    private String clusterGroup;
+
+    public String getClusterGroup() {
+        return clusterGroup;
+    }
+
+    public void setClusterGroup(String clusterGroup) {
+        this.clusterGroup = clusterGroup;
+    }
+
+}
diff --git a/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/ConfigPropertyKeyRequest.java b/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/ConfigPropertyKeyRequest.java
new file mode 100644
index 0000000..f08f330
--- /dev/null
+++ b/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/ConfigPropertyKeyRequest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.karaf.cave.deployer.rest;
+
+public class ConfigPropertyKeyRequest {
+
+    private String pid;
+    private String key;
+    private String jmxUrl;
+    private String karafName;
+    private String user;
+    private String password;
+
+    public String getPid() {
+        return pid;
+    }
+
+    public void setPid(String pid) {
+        this.pid = pid;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public void setKey(String key) {
+        this.key = key;
+    }
+
+    public String getJmxUrl() {
+        return jmxUrl;
+    }
+
+    public void setJmxUrl(String jmxUrl) {
+        this.jmxUrl = jmxUrl;
+    }
+
+    public String getKarafName() {
+        return karafName;
+    }
+
+    public void setKarafName(String karafName) {
+        this.karafName = karafName;
+    }
+
+    public String getUser() {
+        return user;
+    }
+
+    public void setUser(String user) {
+        this.user = user;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+}
diff --git a/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/ConfigPropertyRequest.java b/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/ConfigPropertyRequest.java
new file mode 100644
index 0000000..90d7aba
--- /dev/null
+++ b/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/ConfigPropertyRequest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.karaf.cave.deployer.rest;
+
+public class ConfigPropertyRequest {
+
+    private String pid;
+    private String key;
+    private String value;
+    private String jmxUrl;
+    private String karafName;
+    private String user;
+    private String password;
+
+    public String getPid() {
+        return pid;
+    }
+
+    public void setPid(String pid) {
+        this.pid = pid;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public void setKey(String key) {
+        this.key = key;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public void setValue(String value) {
+        this.value = value;
+    }
+
+    public String getJmxUrl() {
+        return jmxUrl;
+    }
+
+    public void setJmxUrl(String jmxUrl) {
+        this.jmxUrl = jmxUrl;
+    }
+
+    public String getKarafName() {
+        return karafName;
+    }
+
+    public void setKarafName(String karafName) {
+        this.karafName = karafName;
+    }
+
+    public String getUser() {
+        return user;
+    }
+
+    public void setUser(String user) {
+        this.user = user;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+}
diff --git a/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/ConfigRequest.java b/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/ConfigRequest.java
new file mode 100644
index 0000000..39c9765
--- /dev/null
+++ b/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/ConfigRequest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.karaf.cave.deployer.rest;
+
+public class ConfigRequest {
+
+    private String pid;
+    private String jmxUrl;
+    private String karafName;
+    private String user;
+    private String password;
+
+    public String getPid() {
+        return pid;
+    }
+
+    public void setPid(String pid) {
+        this.pid = pid;
+    }
+
+    public String getJmxUrl() {
+        return jmxUrl;
+    }
+
+    public void setJmxUrl(String jmxUrl) {
+        this.jmxUrl = jmxUrl;
+    }
+
+    public String getKarafName() {
+        return karafName;
+    }
+
+    public void setKarafName(String karafName) {
+        this.karafName = karafName;
+    }
+
+    public String getUser() {
+        return user;
+    }
+
+    public void setUser(String user) {
+        this.user = user;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+}
diff --git a/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/ConfigUpdateRequest.java b/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/ConfigUpdateRequest.java
new file mode 100644
index 0000000..256776e
--- /dev/null
+++ b/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/ConfigUpdateRequest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.karaf.cave.deployer.rest;
+
+import org.apache.karaf.cave.deployer.api.Config;
+
+public class ConfigUpdateRequest {
+
+    private Config config;
+    private String jmxUrl;
+    private String karafName;
+    private String user;
+    private String password;
+
+    public Config getConfig() {
+        return config;
+    }
+
+    public void setConfig(Config config) {
+        this.config = config;
+    }
+
+    public String getJmxUrl() {
+        return jmxUrl;
+    }
+
+    public void setJmxUrl(String jmxUrl) {
+        this.jmxUrl = jmxUrl;
+    }
+
+    public String getKarafName() {
+        return karafName;
+    }
+
+    public void setKarafName(String karafName) {
+        this.karafName = karafName;
+    }
+
+    public String getUser() {
+        return user;
+    }
+
+    public void setUser(String user) {
+        this.user = user;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+}
diff --git a/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/DeployRequest.java b/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/DeployRequest.java
new file mode 100644
index 0000000..d98038a
--- /dev/null
+++ b/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/DeployRequest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.karaf.cave.deployer.rest;
+
+public class DeployRequest {
+
+    private String artifactUrl;
+    private String jmxUrl;
+    private String karafName;
+    private String user;
+    private String password;
+
+    public String getArtifactUrl() {
+        return artifactUrl;
+    }
+
+    public void setArtifactUrl(String artifactUrl) {
+        this.artifactUrl = artifactUrl;
+    }
+
+    public String getJmxUrl() {
+        return jmxUrl;
+    }
+
+    public void setJmxUrl(String jmxUrl) {
+        this.jmxUrl = jmxUrl;
+    }
+
+    public String getKarafName() {
+        return karafName;
+    }
+
+    public void setKarafName(String karafName) {
+        this.karafName = karafName;
+    }
+
+    public String getUser() {
+        return user;
+    }
+
+    public void setUser(String user) {
+        this.user = user;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+}
diff --git a/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/DeployerRest.java b/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/DeployerRest.java
new file mode 100644
index 0000000..4d1661a
--- /dev/null
+++ b/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/DeployerRest.java
@@ -0,0 +1,398 @@
+/*
+ * 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.karaf.cave.deployer.rest;
+
+import org.apache.karaf.cave.deployer.api.Bundle;
+import org.apache.karaf.cave.deployer.api.Deployer;
+import org.apache.karaf.cave.deployer.api.Feature;
+import org.apache.karaf.cave.deployer.api.FeaturesRepository;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import java.util.List;
+import java.util.Map;
+
+@Path("/cave/deployer")
+public class DeployerRest {
+
+    private Deployer deployer;
+
+    @Path("/kar/explode")
+    @Consumes("application/json")
+    @POST
+    public void explodeKar(KarExplodeRequest request) throws Exception {
+        deployer.explodeKar(request.getArtifactUrl(), request.getRepositoryUrl());
+    }
+
+    @Path("/artifact/upload")
+    @Consumes("application/json")
+    @POST
+    public void uploadArtifact(UploadRequest request) throws Exception {
+        deployer.uploadArtifact(request.getGroupId(),
+                request.getArtifactId(),
+                request.getVersion(),
+                request.getArtifactUrl(),
+                request.getRepositoryUrl());
+    }
+
+    @Path("/artifact/download")
+    @Consumes("application/json")
+    @POST
+    public void downloadArtifact(String artifactUrl, String localPath) throws Exception {
+        deployer.downloadArtifact(artifactUrl, localPath);
+    }
+
+    @Path("/bundle/deploy")
+    @Consumes("application/json")
+    @POST
+    public void deployBundle(DeployRequest request) throws Exception {
+        deployer.deployBundle(request.getArtifactUrl(),
+                request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    @Path("/bundle/undeploy")
+    @Consumes("application/json")
+    @POST
+    public void undeployBundle(DeployRequest request) throws Exception {
+        deployer.undeployBundle(request.getArtifactUrl(),
+                request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    @Path("/bundle/start")
+    @Consumes("application/json")
+    @POST
+    public void startBundle(DeployRequest request) throws Exception {
+        deployer.startBundle(request.getArtifactUrl(),
+                request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    @Path("/bundle/stop")
+    @Consumes("application/json")
+    @POST
+    public void stopBundle(DeployRequest request) throws Exception {
+        deployer.stopBundle(request.getArtifactUrl(),
+                request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    @Path("/bundles")
+    @Consumes("application/json")
+    @Produces("application/json")
+    @POST
+    public List<Bundle> listBundles(BasicRequest request) throws Exception {
+        return deployer.listBundles(request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    @Path("/kar/install")
+    @Consumes("application/json")
+    @POST
+    public void installKar(DeployRequest request) throws Exception {
+        deployer.installKar(request.getArtifactUrl(),
+                request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    @Path("/kar/uninstall")
+    @Consumes("application/json")
+    @POST
+    public void uninstallKar(DeployRequest request) throws Exception {
+        deployer.uninstallKar(request.getArtifactUrl(),
+                request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    @Path("/kars")
+    @Consumes("application/json")
+    @Produces("application/json")
+    @POST
+    public List<String> listKars(BasicRequest request) throws Exception {
+        return deployer.listKars(request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    @Path("/feature/assemble")
+    @Consumes("application/json")
+    @POST
+    public void assembleFeature(FeatureAssembleRequest request) throws Exception {
+        deployer.assembleFeature(request.getGroupId(),
+                request.getArtifactId(),
+                request.getVersion(),
+                request.getRepositoryUrl(),
+                request.getFeature(),
+                request.getFeatureRepositories(),
+                request.getFeatures(),
+                request.getBundles(),
+                request.getConfigs());
+    }
+
+    @Path("/feature/repository/add")
+    @Consumes("application/json")
+    @POST
+    public void addFeaturesRepository(DeployRequest request) throws Exception {
+        deployer.addFeaturesRepository(request.getArtifactUrl(),
+                request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    @Path("/feature/repository/remove")
+    @Consumes("application/json")
+    @POST
+    public void removeFeaturesRepository(DeployRequest request) throws Exception {
+        deployer.removeFeaturesRepository(request.getArtifactUrl(),
+                request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    @Path("/feature/repositories")
+    @Consumes("application/json")
+    @Produces("application/json")
+    @POST
+    public List<FeaturesRepository> listFeaturesRepositories(BasicRequest request) throws Exception {
+        return deployer.listFeaturesRepositories(request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    @Path("/feature/install")
+    @Consumes("application/json")
+    @POST
+    public void installFeature(DeployRequest request) throws Exception {
+        deployer.installFeature(request.getArtifactUrl(),
+                request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    @Path("/feature/uninstall")
+    @Consumes("application/json")
+    @POST
+    public void uninstallFeature(DeployRequest request) throws Exception {
+        deployer.uninstallFeature(request.getArtifactUrl(),
+                request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    @Path("/features")
+    @Consumes("application/json")
+    @Produces("application/json")
+    @POST
+    public List<Feature> listFeatures(BasicRequest request) throws Exception {
+        return deployer.listFeatures(request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    @Path("/config/create")
+    @Consumes("application/json")
+    @POST
+    public void createConfig(ConfigRequest request) throws Exception {
+        deployer.createConfig(request.getPid(),
+                request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    @Path("/config/delete")
+    @Consumes("application/json")
+    @POST
+    public void deleteConfig(ConfigRequest request) throws Exception {
+        deployer.deleteConfig(request.getPid(),
+                request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    @Path("/config/properties")
+    @Consumes("application/json")
+    @Produces("application/json")
+    @POST
+    public Map<String, String> getConfigProperties(ConfigRequest request) throws Exception {
+        return deployer.getConfigProperties(request.getPid(),
+                request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    @Path("/config/update")
+    @Consumes("application/json")
+    @POST
+    public void updateConfig(ConfigUpdateRequest request) throws Exception {
+        deployer.updateConfig(request.getConfig(),
+                request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    @Path("/config/property/set")
+    @Consumes("application/json")
+    @POST
+    public void setConfigProperty(ConfigPropertyRequest request) throws Exception {
+        deployer.setConfigProperty(request.getPid(),
+                request.getKey(),
+                request.getValue(),
+                request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    @Path("/config/property/get")
+    @Consumes("application/json")
+    @POST
+    public String getConfigProperty(ConfigPropertyKeyRequest request) throws Exception {
+        return deployer.getConfigProperty(request.getPid(),
+                request.getKey(),
+                request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    @Path("/config/property/delete")
+    @Consumes("application/json")
+    @POST
+    public void deleteConfigProperty(ConfigPropertyKeyRequest request) throws Exception {
+        deployer.deleteConfigProperty(request.getPid(),
+                request.getKey(),
+                request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    @Path("/config/property/append")
+    @Consumes("application/json")
+    @POST
+    public void appendConfigProperty(ConfigPropertyRequest request) throws Exception {
+        deployer.appendConfigProperty(request.getPid(),
+                request.getKey(),
+                request.getValue(),
+                request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+    @Path("/cluster/feature/repository/add")
+    @Consumes("application/json")
+    @POST
+    public void clusterAddFeaturesRepository(ClusterDeployRequest request) throws Exception {
+        deployer.clusterAddFeaturesRepository(request.getArtifactUrl(),
+                request.getClusterGroup(),
+                request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    @Path("/cluster/feature/repository/remove")
+    @Consumes("application/json")
+    @POST
+    public void clusterRemoveFeaturesRepository(ClusterDeployRequest request) throws Exception {
+        deployer.clusterRemoveFeaturesRepository(request.getArtifactUrl(),
+                request.getClusterGroup(),
+                request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    @Path("/cluster/feature/install")
+    @Consumes("application/json")
+    @POST
+    public void clusterInstallFeature(ClusterDeployRequest request) throws Exception {
+        deployer.clusterFeatureInstall(request.getArtifactUrl(),
+                request.getClusterGroup(),
+                request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    @Path("/cluster/feature/uninstall")
+    @Consumes("application/json")
+    @POST
+    public void clusterUninstallFeature(ClusterDeployRequest request) throws Exception {
+        deployer.clusterFeatureUninstall(request.getArtifactUrl(),
+                request.getClusterGroup(),
+                request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    @Path("/cluster/nodes")
+    @Consumes("application/json")
+    @Produces("application/json")
+    @POST
+    public List<String> clusterNodes(DeployRequest request) throws Exception {
+        return deployer.clusterNodes(request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    @Path("/cluster/groups")
+    @Consumes("application/json")
+    @Produces("application/json")
+    @POST
+    public Map<String, List<String>> clusterGroups(DeployRequest request) throws Exception {
+        return deployer.clusterGroups(request.getJmxUrl(),
+                request.getKarafName(),
+                request.getUser(),
+                request.getPassword());
+    }
+
+    public void setDeployer(Deployer deployer) {
+        this.deployer = deployer;
+    }
+
+}
diff --git a/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/FeatureAssembleRequest.java b/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/FeatureAssembleRequest.java
new file mode 100644
index 0000000..41fd96d
--- /dev/null
+++ b/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/FeatureAssembleRequest.java
@@ -0,0 +1,106 @@
+/*
+ * 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.karaf.cave.deployer.rest;
+
+import org.apache.karaf.cave.deployer.api.Config;
+
+import java.util.List;
+
+public class FeatureAssembleRequest {
+
+    private String groupId;
+    private String artifactId;
+    private String version;
+    private String feature;
+    private String repositoryUrl;
+    private List<String> featureRepositories;
+    private List<String> features;
+    private List<String> bundles;
+    private List<Config> configs;
+
+    public String getGroupId() {
+        return groupId;
+    }
+
+    public void setGroupId(String groupId) {
+        this.groupId = groupId;
+    }
+
+    public String getArtifactId() {
+        return artifactId;
+    }
+
+    public void setArtifactId(String artifactId) {
+        this.artifactId = artifactId;
+    }
+
+    public String getVersion() {
+        return version;
+    }
+
+    public void setVersion(String version) {
+        this.version = version;
+    }
+
+    public String getFeature() {
+        return feature;
+    }
+
+    public void setFeature(String feature) {
+        this.feature = feature;
+    }
+
+    public String getRepositoryUrl() {
+        return repositoryUrl;
+    }
+
+    public void setRepositoryUrl(String repositoryUrl) {
+        this.repositoryUrl = repositoryUrl;
+    }
+
+    public List<String> getFeatureRepositories() {
+        return featureRepositories;
+    }
+
+    public void setFeatureRepositories(List<String> featureRepositories) {
+        this.featureRepositories = featureRepositories;
+    }
+
+    public List<String> getFeatures() {
+        return features;
+    }
+
+    public void setFeatures(List<String> features) {
+        this.features = features;
+    }
+
+    public List<String> getBundles() {
+        return bundles;
+    }
+
+    public void setBundles(List<String> bundles) {
+        this.bundles = bundles;
+    }
+    public List<Config> getConfigs() {
+        return configs;
+    }
+
+    public void setConfigs(List<Config> configs) {
+        this.configs = configs;
+    }
+
+}
diff --git a/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/KarExplodeRequest.java b/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/KarExplodeRequest.java
new file mode 100644
index 0000000..28d5568
--- /dev/null
+++ b/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/KarExplodeRequest.java
@@ -0,0 +1,40 @@
+/*
+ * 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.karaf.cave.deployer.rest;
+
+public class KarExplodeRequest {
+
+    private String artifactUrl;
+    private String repositoryUrl;
+
+    public String getArtifactUrl() {
+        return artifactUrl;
+    }
+
+    public void setArtifactUrl(String artifactUrl) {
+        this.artifactUrl = artifactUrl;
+    }
+
+    public String getRepositoryUrl() {
+        return repositoryUrl;
+    }
+
+    public void setRepositoryUrl(String repositoryUrl) {
+        this.repositoryUrl = repositoryUrl;
+    }
+
+}
diff --git a/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/UploadRequest.java b/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/UploadRequest.java
new file mode 100644
index 0000000..b128001
--- /dev/null
+++ b/deployer/rest/src/main/java/org/apache/karaf/cave/deployer/rest/UploadRequest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.karaf.cave.deployer.rest;
+
+public class UploadRequest {
+
+    private String groupId;
+    private String artifactId;
+    private String version;
+    private String artifactUrl;
+    private String repositoryUrl;
+
+    public String getGroupId() {
+        return groupId;
+    }
+
+    public void setGroupId(String groupId) {
+        this.groupId = groupId;
+    }
+
+    public String getArtifactId() {
+        return artifactId;
+    }
+
+    public void setArtifactId(String artifactId) {
+        this.artifactId = artifactId;
+    }
+
+    public String getVersion() {
+        return version;
+    }
+
+    public void setVersion(String version) {
+        this.version = version;
+    }
+
+    public String getArtifactUrl() {
+        return artifactUrl;
+    }
+
+    public void setArtifactUrl(String artifactUrl) {
+        this.artifactUrl = artifactUrl;
+    }
+
+    public String getRepositoryUrl() {
+        return repositoryUrl;
+    }
+
+    public void setRepositoryUrl(String repositoryUrl) {
+        this.repositoryUrl = repositoryUrl;
+    }
+
+}
diff --git a/deployer/rest/src/main/resources/OSGI-INF/blueprint/rest.xml b/deployer/rest/src/main/resources/OSGI-INF/blueprint/rest.xml
new file mode 100644
index 0000000..524c3cf
--- /dev/null
+++ b/deployer/rest/src/main/resources/OSGI-INF/blueprint/rest.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+-->
+<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
+           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+           xmlns:jaxrs="http://cxf.apache.org/blueprint/jaxrs"
+           xmlns:cxf="http://cxf.apache.org/blueprint/core"
+           xsi:schemaLocation="
+             http://www.osgi.org/xmlns/blueprint/v1.0.0 http://www.osgi.org/xmlns/blueprint/v1.0.0/blueprint.xsd
+             http://cxf.apache.org/blueprint/jaxrs http://cxf.apache.org/schemas/blueprint/jaxrs.xsd
+             http://cxf.apache.org/blueprint/core http://cxf.apache.org/schemas/blueprint/core.xsd
+             ">
+
+    <cxf:bus>
+        <cxf:features>
+            <cxf:logging/>
+        </cxf:features>
+    </cxf:bus>
+
+    <jaxrs:server id="deployerRest" address="/">
+        <jaxrs:serviceBeans>
+            <ref component-id="serviceBean" />
+        </jaxrs:serviceBeans>
+        <jaxrs:providers>
+            <bean class="com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider"/>
+        </jaxrs:providers>
+    </jaxrs:server>
+
+    <reference id="deployer" interface="org.apache.karaf.cave.deployer.api.Deployer"/>
+
+    <bean id="serviceBean" class="org.apache.karaf.cave.deployer.rest.DeployerRest">
+        <property name="deployer" ref="deployer"/>
+    </bean>
+
+</blueprint>
diff --git a/deployer/rest/src/test/java/org/apache/karaf/cave/deployer/rest/DeployerRestTest.java b/deployer/rest/src/test/java/org/apache/karaf/cave/deployer/rest/DeployerRestTest.java
new file mode 100644
index 0000000..557f480
--- /dev/null
+++ b/deployer/rest/src/test/java/org/apache/karaf/cave/deployer/rest/DeployerRestTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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.karaf.cave.deployer.rest;
+
+import org.apache.karaf.cave.deployer.api.Deployer;
+import org.apache.karaf.cave.deployer.service.impl.DeployerImpl;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.util.Arrays;
+
+public class DeployerRestTest {
+
+    private DeployerRest rest;
+
+    @Before
+    public void init() throws Exception {
+        System.setProperty("java.protocol.handler.pkgs", "org.ops4j.pax.url");
+        System.setProperty("java.io.tmpdir", "target");
+        Deployer deployer = new DeployerImpl();
+        rest = new DeployerRest();
+        rest.setDeployer(deployer);
+    }
+
+    @Test
+    @Ignore
+    public void installKarTest() throws Exception {
+        System.out.println("Deploy a kar in a Karaf instance.");
+        System.out.println("WARNING: this test requires a running Karaf instance");
+
+        UploadRequest uploadRequest = new UploadRequest();
+        uploadRequest.setRepositoryUrl("file:target/test/repository");
+        uploadRequest.setArtifactUrl("file:src/test/resources/test.kar");
+        uploadRequest.setGroupId("kar-test");
+        uploadRequest.setArtifactId("kar-test");
+        uploadRequest.setVersion("1.0-SNAPSHOT");
+        rest.uploadArtifact(uploadRequest);
+
+        DeployRequest request = new DeployRequest();
+        request.setArtifactUrl("mvn:kar-test/kar-test/1.0-SNAPSHOT/kar");
+        request.setJmxUrl("service:jmx:rmi:///jndi/rmi://localhost:1099/karaf-root");
+        request.setKarafName("root");
+        request.setUser("karaf");
+        request.setPassword("karaf");
+        rest.installKar(request);
+    }
+
+    @Test
+    public void explodeKarTest() throws Exception {
+        System.out.println("This test is step 1 in the use case:");
+        System.out.println("\t- User creates a kar locally");
+        System.out.println("\t- The kar is uploaded on Maven repo (using mvn deploy:deploy-file or API");
+        System.out.println("\t- The kar is exploded on the Maven repo");
+
+        UploadRequest uploadRequest = new UploadRequest();
+        uploadRequest.setRepositoryUrl("file:target/test/repository");
+        uploadRequest.setArtifactUrl("file:src/test/resources/test.kar");
+        uploadRequest.setGroupId("kar-test");
+        uploadRequest.setArtifactId("kar-test");
+        uploadRequest.setVersion("1.0-SNAPSHOT");
+        rest.uploadArtifact(uploadRequest);
+
+        KarExplodeRequest karExplodeRequest = new KarExplodeRequest();
+        // TODO: the artifact URL should be the one on Maven repository
+        // To simplify we use test resources location directly
+        karExplodeRequest.setArtifactUrl("file:src/test/resources/test.kar");
+        karExplodeRequest.setRepositoryUrl("file:target/test/repository");
+        rest.explodeKar(karExplodeRequest);
+    }
+
+    @Test
+    @Ignore
+    public void installFeatureTest() throws Exception {
+        System.out.println("This test is the following step in the use case (if user doesn't want to create an uber feature)");
+        System.out.println("WARNING: This test requires a running Karaf instance");
+        System.out.println("- It registers a features repository");
+        System.out.println("- Then installs a feature");
+
+        DeployRequest deployRequest = new DeployRequest();
+        deployRequest.setArtifactUrl("mvn:org.apache.camel.karaf/apache-camel/2.17.1/xml/features");
+        deployRequest.setJmxUrl("service:jmx:rmi:///jndi/rmi://localhost:1099/karaf-root");
+        deployRequest.setKarafName("root");
+        deployRequest.setUser("karaf");
+        deployRequest.setPassword("karaf");
+        rest.addFeaturesRepository(deployRequest);
+
+        deployRequest = new DeployRequest();
+        deployRequest.setArtifactUrl("camel-blueprint");
+        deployRequest.setJmxUrl("service:jmx:rmi:///jndi/rmi://localhost:1099/karaf-root");
+        deployRequest.setKarafName("root");
+        deployRequest.setUser("karaf");
+        deployRequest.setPassword("karaf");
+        rest.installFeature(deployRequest);
+    }
+
+    @Test
+    @Ignore
+    public void listingTest() throws Exception {
+        System.out.println("WARNING: this test requires an active container.");
+
+        BasicRequest request = new BasicRequest();
+        request.setJmxUrl("service:jmx:rmi:///jndi/rmi://localhost:1099/karaf-root");
+        request.setKarafName("root");
+        request.setUser("karaf");
+        request.setPassword("karaf");
+
+        System.out.println(rest.listBundles(request));
+        System.out.println(rest.listKars(request));
+        System.out.println(rest.listFeaturesRepositories(request));
+        System.out.println(rest.listFeatures(request));
+    }
+
+    @Test
+    @Ignore
+    public void assembleFeatureTest() throws Exception {
+        System.out.println("This test is a second step in the use case.");
+        System.out.println("The user creates a \"meta\" feature, assembling existing feature");
+        System.out.println("Then he can install this \"meta\" feature");
+
+        FeatureAssembleRequest assembleRequest = new FeatureAssembleRequest();
+        assembleRequest.setVersion("1.0-SNAPSHOT");
+        assembleRequest.setArtifactId("business-feature");
+        assembleRequest.setFeature("business-feature");
+        assembleRequest.setGroupId("business-feature");
+        assembleRequest.setRepositoryUrl("file:" + System.getProperty("user.home") + "/.m2/repository");
+        assembleRequest.setFeatureRepositories(Arrays.asList(new String[]{ "mvn:org.apache.camel.karaf/apache-camel/2.17.1/xml/features" }));
+        assembleRequest.setFeatures(Arrays.asList(new String[]{ "camel-blueprint", "camel-stream" }));
+        rest.assembleFeature(assembleRequest);
+
+        DeployRequest deployRequest = new DeployRequest();
+        deployRequest.setArtifactUrl("mvn:business-feature/business-feature/1.0-SNAPSHOT/xml/features");
+        deployRequest.setJmxUrl("service:jmx:rmi:///jndi/rmi://localhost:1099/karaf-root");
+        deployRequest.setKarafName("root");
+        deployRequest.setUser("karaf");
+        deployRequest.setPassword("karaf");
+        rest.addFeaturesRepository(deployRequest);
+
+        deployRequest = new DeployRequest();
+        deployRequest.setArtifactUrl("business-feature");
+        deployRequest.setJmxUrl("service:jmx:rmi:///jndi/rmi://localhost:1099/karaf-root");
+        deployRequest.setKarafName("root");
+        deployRequest.setUser("karaf");
+        deployRequest.setPassword("karaf");
+        rest.installFeature(deployRequest);
+    }
+
+}
diff --git a/deployer/rest/src/test/resources/test.kar b/deployer/rest/src/test/resources/test.kar
new file mode 100644
index 0000000..f2b3c5b
Binary files /dev/null and b/deployer/rest/src/test/resources/test.kar differ
diff --git a/deployer/service/pom.xml b/deployer/service/pom.xml
new file mode 100644
index 0000000..c568935
--- /dev/null
+++ b/deployer/service/pom.xml
@@ -0,0 +1,361 @@
+<?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">
+
+    <!--
+
+        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.
+    -->
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.karaf.cave</groupId>
+        <artifactId>org.apache.karaf.cave.deployer</artifactId>
+        <version>4.1.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <groupId>org.apache.karaf.cave.deployer</groupId>
+    <artifactId>org.apache.karaf.cave.deployer.service</artifactId>
+    <name>Apache Karaf :: Cave :: Deployer :: Service</name>
+    <packaging>bundle</packaging>
+
+    <properties>
+        <maven.version>3.1.1</maven.version>
+        <aether.version>1.0.1.v20141111</aether.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.karaf.cave.deployer</groupId>
+            <artifactId>org.apache.karaf.cave.deployer.api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <!-- Maven dependencies -->
+        <dependency>
+            <groupId>org.apache.maven</groupId>
+            <artifactId>maven-core</artifactId>
+            <version>${maven.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.maven</groupId>
+            <artifactId>maven-compat</artifactId>
+            <version>${maven.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.apache.maven.wagon</groupId>
+                    <artifactId>wagon-provider-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.maven</groupId>
+            <artifactId>maven-embedder</artifactId>
+            <version>${maven.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.maven</groupId>
+            <artifactId>maven-aether-provider</artifactId>
+            <version>${maven.version}</version>
+        </dependency>
+
+        <!-- Eclipse Aether -->
+        <dependency>
+            <groupId>org.eclipse.aether</groupId>
+            <artifactId>aether-connector-wagon</artifactId>
+            <version>0.9.0.M2</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.apache.maven.wagon</groupId>
+                    <artifactId>wagon-provider-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.maven.wagon</groupId>
+            <artifactId>wagon-provider-api</artifactId>
+            <version>2.4</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.aether</groupId>
+            <artifactId>aether-api</artifactId>
+            <version>${aether.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.aether</groupId>
+            <artifactId>aether-spi</artifactId>
+            <version>${aether.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.aether</groupId>
+            <artifactId>aether-util</artifactId>
+            <version>${aether.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.aether</groupId>
+            <artifactId>aether-impl</artifactId>
+            <version>${aether.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.aether</groupId>
+            <artifactId>aether-connector-basic</artifactId>
+            <version>${aether.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.aether</groupId>
+            <artifactId>aether-transport-file</artifactId>
+            <version>${aether.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.aether</groupId>
+            <artifactId>aether-transport-http</artifactId>
+            <version>${aether.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.apache.httpcomponents</groupId>
+                    <artifactId>httpclient</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-api</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>jcl-over-slf4j</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpcore-osgi</artifactId>
+            <version>4.2.5</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>commons-codec</groupId>
+                    <artifactId>commons-codec</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient-osgi</artifactId>
+            <version>4.2.5</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>commons-codec</groupId>
+                    <artifactId>commons-codec</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.sisu</groupId>
+            <artifactId>org.eclipse.sisu.plexus</artifactId>
+            <version>0.1.1</version>
+            <optional>true</optional>
+            <exclusions>
+                <exclusion>
+                    <groupId>javax.enterprise</groupId>
+                    <artifactId>cdi-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>javax.enterprise</groupId>
+            <artifactId>cdi-api</artifactId>
+            <version>1.2</version>
+        </dependency>
+        <dependency>
+            <groupId>org.sonatype.sisu</groupId>
+            <artifactId>sisu-guice</artifactId>
+            <version>3.1.6</version>
+            <classifier>no_aop</classifier>
+            <optional>true</optional>
+            <exclusions>
+                <exclusion>
+                    <groupId>aopalliance</groupId>
+                    <artifactId>aopalliance</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.google.code.findbugs</groupId>
+                    <artifactId>jsr305</artifactId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>guava</artifactId>
+                    <groupId>com.google.guava</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>javax.inject</artifactId>
+                    <groupId>javax.inject</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.sonatype.sisu</groupId>
+            <artifactId>sisu-inject-bean</artifactId>
+            <version>2.3.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.sonatype.sisu</groupId>
+            <artifactId>sisu-guava</artifactId>
+            <version>0.9.9</version>
+        </dependency>
+
+        <!-- Guava -->
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>18.0</version>
+        </dependency>
+
+        <!-- OSGi & Karaf-->
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.karaf</groupId>
+            <artifactId>org.apache.karaf.util</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.karaf.features</groupId>
+            <artifactId>org.apache.karaf.features.core</artifactId>
+            <version>4.0.2</version>
+        </dependency>
+
+        <!-- Logging -->
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+
+        <!-- Testing -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.12</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.url</groupId>
+            <artifactId>pax-url-aether</artifactId>
+            <version>2.4.6</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-log4j12</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.karaf.tooling</groupId>
+                <artifactId>karaf-services-maven-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <inherited>true</inherited>
+                <configuration>
+                    <instructions>
+                        <Import-Package>
+                            org.apache.karaf.cave.deployer.api,
+                            org.slf4j;resolution:=optional,
+                            org.junit;resolution:=optional,
+                            org.testng*;resolution:=optional,
+                            junit.framework.*;resolution:=optional,
+                            org.apache.maven.model.*;resolution:=optional,
+                            org.apache.maven.artifact.*;resolution:=optional,
+                            org.apache.maven.cli.*;resolution:=optional,
+                            org.apache.maven.settings.merge.*;resolution:=optional,
+                            org.apache.maven.wagon.events.*;resolution:=optional,
+                            org.apache.commons.cli.*;resolution:=optional,
+                            org.apache.tools.ant.*;resolution:=optional,
+                            org.codehaus.plexus.component.repository.exception.*,
+                            org.codehaus.plexus.component.annotations.*;resolution:=optional,
+                            org.codehaus.plexus.util.*;resolution:=optional,
+                            org.sonatype.plexus.components.cipher.*;resolution:=optional,
+                            org.sonatype.plexus.components.sec.dispatcher.*;resolution:=optional,
+                            hudson.maven.*;resolution:=optional,
+                            org.eclipse.aether.util.repository.layout.*;resolution:=optional,
+                            org.apache.maven.wagon.*;resolution:=optional,
+                            ch.qos.logback*;resolution:=optional,
+                            *
+                        </Import-Package>
+                        <Private-Package>
+                            org.apache.karaf.cave.deployer.service.impl,
+                            org.apache.karaf.features.internal.model,
+                            org.apache.felix.utils.version,
+                            org.apache.karaf.util
+                        </Private-Package>
+                        <Embed-Dependency>
+                            sisu-guice,
+                            sisu-guava,
+                            sisu-inject-bean,
+                            javax.interceptor-api,
+                            javax.el-api,
+                            cdi-api,
+                            aether-transport-http,
+                            aether-transport-file,
+                            aether-connector-basic,
+                            wagon-provider-api,
+                            aether-util,
+                            aether-spi,
+                            aether-impl,
+                            aether-api,
+                            aether-connector-wagon,
+                            maven-aether-provider,
+                            maven-model,
+                            maven-compat,
+                            ant,
+                            maven-settings-builder,
+                            maven-settings,
+                            org.eclipse.sisu.plexus,
+                            org.eclipse.sisu.inject,
+                            plexus-component-annotations,
+                            plexus-sec-dispatcher,
+                            plexus-cipher,
+                            commons-cli,
+                            maven-embedder,
+                            plexus-classworlds,
+                            maven-model-builder,
+                            maven-artifact,
+                            maven-repository-metadata,
+                            maven-core,
+                            maven-plugin-api,
+                            plexus-utils,
+                            plexus-interpolation
+                        </Embed-Dependency>
+                        <Embed-Directory>jars</Embed-Directory>
+                        <Embed-Transitive>true</Embed-Transitive>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
\ No newline at end of file
diff --git a/deployer/service/src/main/java/org/apache/karaf/cave/deployer/service/impl/Activator.java b/deployer/service/src/main/java/org/apache/karaf/cave/deployer/service/impl/Activator.java
new file mode 100644
index 0000000..df6d74f
--- /dev/null
+++ b/deployer/service/src/main/java/org/apache/karaf/cave/deployer/service/impl/Activator.java
@@ -0,0 +1,35 @@
+/*
+ * 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.karaf.cave.deployer.service.impl;
+
+import org.apache.karaf.cave.deployer.api.Deployer;
+import org.apache.karaf.util.tracker.BaseActivator;
+import org.apache.karaf.util.tracker.annotation.ProvideService;
+import org.apache.karaf.util.tracker.annotation.Services;
+
+@Services(
+        provides = { @ProvideService(Deployer.class) }
+)
+public class Activator extends BaseActivator {
+
+    @Override
+    public void doStart() {
+        DeployerImpl deployer = new DeployerImpl();
+        register(Deployer.class, deployer);
+    }
+
+}
diff --git a/deployer/service/src/main/java/org/apache/karaf/cave/deployer/service/impl/ConsoleRepositoryListener.java b/deployer/service/src/main/java/org/apache/karaf/cave/deployer/service/impl/ConsoleRepositoryListener.java
new file mode 100644
index 0000000..7db240c
--- /dev/null
+++ b/deployer/service/src/main/java/org/apache/karaf/cave/deployer/service/impl/ConsoleRepositoryListener.java
@@ -0,0 +1,105 @@
+/*
+ * 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.karaf.cave.deployer.service.impl;
+
+import org.eclipse.aether.AbstractRepositoryListener;
+import org.eclipse.aether.RepositoryEvent;
+
+import java.io.PrintStream;
+
+public class ConsoleRepositoryListener extends AbstractRepositoryListener {
+
+    private PrintStream out;
+
+    public ConsoleRepositoryListener() {
+        this(null);
+    }
+
+    public ConsoleRepositoryListener(PrintStream out) {
+        this.out = (out != null) ? out : System.out;
+    }
+
+    public void artifactDeployed(RepositoryEvent event) {
+        out.println("Deployed " + event.getArtifact() + " to " + event.getRepository());
+    }
+
+    public void artifactDeploying(RepositoryEvent event) {
+        out.println("Deploying " + event.getArtifact() + " to " + event.getRepository());
+    }
+
+    public void artifactDescriptorInvalid(RepositoryEvent event) {
+        out.println("Invalid artifact descriptor for " + event.getArtifact() + ": "
+                + event.getException().getMessage());
+    }
+
+    public void artifactDescriptorMissing(RepositoryEvent event) {
+        out.println("Missing artifact descriptor for " + event.getArtifact());
+    }
+
+    public void artifactInstalled(RepositoryEvent event) {
+        out.println("Installed " + event.getArtifact() + " to " + event.getFile());
+    }
+
+    public void artifactInstalling(RepositoryEvent event) {
+        out.println("Installing " + event.getArtifact() + " to " + event.getFile());
+    }
+
+    public void artifactResolved(RepositoryEvent event) {
+        out.println("Resolved artifact " + event.getArtifact() + " from " + event.getRepository());
+    }
+
+    public void artifactDownloading(RepositoryEvent event) {
+        out.println("Downloading artifact " + event.getArtifact() + " from " + event.getRepository());
+    }
+
+    public void artifactDownloaded(RepositoryEvent event) {
+        out.println("Downloaded artifact " + event.getArtifact() + " from " + event.getRepository());
+    }
+
+    public void artifactResolving(RepositoryEvent event) {
+        out.println("Resolving artifact " + event.getArtifact());
+    }
+
+    public void metadataDeployed(RepositoryEvent event) {
+        out.println("Deployed " + event.getMetadata() + " to " + event.getRepository());
+    }
+
+    public void metadataDeploying(RepositoryEvent event) {
+        out.println("Deploying " + event.getMetadata() + " to " + event.getRepository());
+    }
+
+    public void metadataInstalled(RepositoryEvent event) {
+        out.println("Installed " + event.getMetadata() + " to " + event.getFile());
+    }
+
+    public void metadataInstalling(RepositoryEvent event) {
+        out.println("Installing " + event.getMetadata() + " to " + event.getFile());
+    }
+
+    public void metadataInvalid(RepositoryEvent event) {
+        out.println("Invalid metadata " + event.getMetadata());
+    }
+
+    public void metadataResolved(RepositoryEvent event) {
+        out.println("Resolved metadata " + event.getMetadata() + " from " + event.getRepository());
+    }
+
+    public void metadataResolving(RepositoryEvent event) {
+        out.println("Resolving metadata " + event.getMetadata() + " from " + event.getRepository());
+    }
+
+}
diff --git a/deployer/service/src/main/java/org/apache/karaf/cave/deployer/service/impl/ConsoleTransferListener.java b/deployer/service/src/main/java/org/apache/karaf/cave/deployer/service/impl/ConsoleTransferListener.java
new file mode 100644
index 0000000..3b8c37d
--- /dev/null
+++ b/deployer/service/src/main/java/org/apache/karaf/cave/deployer/service/impl/ConsoleTransferListener.java
@@ -0,0 +1,143 @@
+/*
+ * 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.karaf.cave.deployer.service.impl;
+
+import org.eclipse.aether.transfer.AbstractTransferListener;
+import org.eclipse.aether.transfer.TransferEvent;
+import org.eclipse.aether.transfer.TransferResource;
+
+import java.io.PrintStream;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class ConsoleTransferListener extends AbstractTransferListener {
+
+    private PrintStream out;
+
+    private Map<TransferResource, Long> downloads = new ConcurrentHashMap<>();
+
+    private int lastLength;
+
+    public ConsoleTransferListener() {
+        this(null);
+    }
+
+    public ConsoleTransferListener(PrintStream out) {
+        this.out = (out != null) ? out : System.out;
+    }
+
+    @Override
+    public void transferInitiated(TransferEvent event) {
+        String message = event.getRequestType() == TransferEvent.RequestType.PUT ? "Uploading" : "Downloading";
+
+        out.println(message + ": " + event.getResource().getRepositoryUrl() + event.getResource().getResourceName());
+    }
+
+    @Override
+    public void transferProgressed(TransferEvent event) {
+        TransferResource resource = event.getResource();
+        downloads.put(resource, Long.valueOf(event.getTransferredBytes()));
+
+        StringBuilder buffer = new StringBuilder(64);
+
+        for (Map.Entry<TransferResource, Long> entry : downloads.entrySet()) {
+            long total = entry.getKey().getContentLength();
+            long complete = entry.getValue().longValue();
+
+            buffer.append(getStatus(complete, total)).append("  ");
+        }
+
+        int pad = lastLength - buffer.length();
+        lastLength = buffer.length();
+        pad(buffer, pad);
+        buffer.append('\r');
+
+        out.print(buffer);
+    }
+
+    private String getStatus(long complete, long total) {
+        if (total >= 1024) {
+            return toKB(complete) + "/" + toKB(total) + " KB ";
+        } else if (total >= 0) {
+            return complete + "/" + total + " B ";
+        } else if (complete >= 1024) {
+            return toKB(complete) + " KB ";
+        } else {
+            return complete + " B ";
+        }
+    }
+
+    private void pad(StringBuilder buffer, int spaces) {
+        String block = "                                        ";
+        while (spaces > 0) {
+            int n = Math.min(spaces, block.length());
+            buffer.append(block, 0, n);
+            spaces -= n;
+        }
+    }
+
+    @Override
+    public void transferSucceeded(TransferEvent event) {
+        transferCompleted(event);
+
+        TransferResource resource = event.getResource();
+        long contentLength = event.getTransferredBytes();
+        if (contentLength >= 0) {
+            String type = (event.getRequestType() == TransferEvent.RequestType.PUT ? "Uploaded" : "Downloaded");
+            String len = contentLength >= 1024 ? toKB(contentLength) + " KB" : contentLength + " B";
+
+            String throughput = "";
+            long duration = System.currentTimeMillis() - resource.getTransferStartTime();
+            if (duration > 0) {
+                DecimalFormat format = new DecimalFormat("0.0", new DecimalFormatSymbols(Locale.ENGLISH));
+                double kbPerSec = (contentLength / 1024.0) / (duration / 1000.0);
+                throughput = " at " + format.format(kbPerSec) + " KB/sec";
+            }
+
+            out.println(type + ": " + resource.getRepositoryUrl() + resource.getResourceName() + " (" + len
+                    + throughput + ")");
+        }
+    }
+
+    @Override
+    public void transferFailed(TransferEvent event) {
+        transferCompleted(event);
+
+        event.getException().printStackTrace(out);
+    }
+
+    private void transferCompleted(TransferEvent event) {
+        downloads.remove(event.getResource());
+
+        StringBuilder buffer = new StringBuilder(64);
+        pad(buffer, lastLength);
+        buffer.append('\r');
+        out.print(buffer);
+    }
+
+    public void transferCorrupted(TransferEvent event) {
+        event.getException().printStackTrace(out);
+    }
+
+    protected long toKB(long bytes) {
+        return (bytes + 1023) / 1024;
+    }
+
+}
diff --git a/deployer/service/src/main/java/org/apache/karaf/cave/deployer/service/impl/DeployerImpl.java b/deployer/service/src/main/java/org/apache/karaf/cave/deployer/service/impl/DeployerImpl.java
new file mode 100644
index 0000000..56561db
--- /dev/null
+++ b/deployer/service/src/main/java/org/apache/karaf/cave/deployer/service/impl/DeployerImpl.java
@@ -0,0 +1,882 @@
+/*
+ * 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.karaf.cave.deployer.service.impl;
+
+import com.google.common.io.Files;
+import org.apache.karaf.cave.deployer.api.Deployer;
+import org.apache.karaf.cave.deployer.api.FeaturesRepository;
+import org.apache.karaf.features.internal.model.*;
+import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
+import org.eclipse.aether.deployment.DeployRequest;
+import org.eclipse.aether.impl.DefaultServiceLocator;
+import org.eclipse.aether.installation.InstallRequest;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.LocalRepositoryManager;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
+import org.eclipse.aether.spi.connector.transport.TransporterFactory;
+import org.eclipse.aether.transport.file.FileTransporterFactory;
+import org.eclipse.aether.transport.http.HttpTransporterFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.management.MBeanServerConnection;
+import javax.management.ObjectName;
+import javax.management.openmbean.CompositeData;
+import javax.management.openmbean.TabularData;
+import javax.management.remote.JMXConnector;
+import javax.management.remote.JMXConnectorFactory;
+import javax.management.remote.JMXServiceURL;
+import java.io.*;
+import java.net.URI;
+import java.util.*;
+import java.util.jar.JarInputStream;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.zip.ZipEntry;
+
+public class DeployerImpl implements Deployer {
+
+    private final static Logger LOGGER  = LoggerFactory.getLogger(DeployerImpl.class);
+
+    private final static Pattern mvnPattern = Pattern.compile("mvn:([^/ ]+)/([^/ ]+)/([^/ ]*)(/([^/ ]+)(/([^/ ]+))?)?");
+
+    @Override
+    public void downloadArtifact(String artifactUrl, String localUrl) throws Exception {
+        InputStream is = new URI(artifactUrl).toURL().openStream();
+        File file = new File(localUrl);
+        file.getParentFile().mkdirs();
+        FileOutputStream os = new FileOutputStream(file);
+        copyStream(is, os);
+    }
+
+    @Override
+    public void explodeKar(String karUrl, String repositoryUrl) throws Exception {
+        File tempDirectory = Files.createTempDir();
+        extract(karUrl, tempDirectory);
+        File karRepository = new File(tempDirectory, "repository");
+        browseKar(karRepository, karRepository.getPath(), repositoryUrl);
+    }
+
+    protected void browseKar(File entry, String basePath, String repositoryUrl) {
+        if (entry.isDirectory()) {
+            File[] files = entry.listFiles();
+            for (File file : files) {
+                if (file.isDirectory()) {
+                    browseKar(file, basePath, repositoryUrl);
+                } else {
+                    String path = file.getParentFile().getParentFile().getParentFile().getPath();
+                    if (path.startsWith(basePath)) {
+                        path = path.substring(basePath.length() + 1);
+                    }
+                    path = path.replace('/', '.');
+                    String groupId = path;
+                    String artifactId = file.getParentFile().getParentFile().getName();
+                    String version = file.getParentFile().getName();
+                    String extension = file.getName().substring(file.getName().lastIndexOf('.') + 1);
+                    try {
+                        uploadArtifact(groupId, artifactId, version, extension, file, repositoryUrl);
+                    } catch (Exception e) {
+                        LOGGER.warn("Can't upload artifact {}/{}/{}/{}", new String[]{groupId, artifactId, version, extension}, e);
+                    }
+                }
+            }
+        }
+    }
+
+    protected static boolean isMavenUrl(String url) {
+        Matcher m = mvnPattern.matcher(url);
+        return m.matches();
+    }
+
+    protected static Map<String, String> parse(String url) {
+        Matcher m = mvnPattern.matcher(url);
+        if (!m.matches()) {
+            return null;
+        }
+        Map<String, String> result = new HashMap<String, String>();
+        result.put("groupId", m.group(1));
+        result.put("artifactId", m.group(2));
+        result.put("version", m.group(3));
+        if (m.group(5) == null) {
+            result.put("extension", "jar");
+        } else {
+            result.put("extension", m.group(5));
+        }
+        result.put("classifier", m.group(7));
+        return result;
+    }
+
+    public void extract(String url, File baseDir) throws Exception {
+        InputStream is = null;
+        JarInputStream zipIs = null;
+
+        File repoDir = new File(baseDir, "repository");
+        File resourceDir = new File(baseDir, "resource");
+
+        try {
+            is = new URI(url).toURL().openStream();
+            repoDir.mkdirs();
+
+            zipIs = new JarInputStream(is);
+            boolean scanForRepos = true;
+
+            ZipEntry entry = zipIs.getNextEntry();
+            while (entry != null) {
+                if (entry.getName().startsWith("repository")) {
+                    String path = entry.getName().substring("repository/".length());
+                    File destFile = new File(repoDir, path);
+                    extract(zipIs, entry, destFile);
+                }
+
+                if (entry.getName().startsWith("resource")) {
+                    String path = entry.getName().substring("resource/".length());
+                    File destFile = new File(resourceDir, path);
+                    extract(zipIs, entry, destFile);
+                }
+                entry = zipIs.getNextEntry();
+            }
+        } finally {
+            if (zipIs != null) {
+                zipIs.close();
+            }
+            if (is != null) {
+                is.close();
+            }
+        }
+    }
+
+    private static File extract(InputStream is, ZipEntry zipEntry, File dest) throws Exception {
+        if (zipEntry.isDirectory()) {
+            dest.mkdirs();
+        } else {
+            dest.getParentFile().mkdirs();
+            FileOutputStream out = new FileOutputStream(dest);
+            copyStream(is, out);
+            out.close();
+        }
+        return dest;
+    }
+
+    static long copyStream(InputStream input, OutputStream output) throws IOException {
+        byte[] buffer = new byte[10000];
+        long count = 0;
+        int n = 0;
+        while (-1 != (n = input.read(buffer))) {
+            output.write(buffer, 0, n);
+            count += n;
+        }
+        return count;
+    }
+
+    @Override
+    public void uploadArtifact(String groupId,
+                               String artifactId,
+                               String version,
+                               String artifactUrl,
+                               String repositoryUrl) throws Exception {
+
+        Map<String, String> coordonates = new HashMap<String, String>();
+        if (isMavenUrl(artifactUrl)) {
+            coordonates = parse(artifactUrl);
+        } else {
+            int index = artifactUrl.lastIndexOf('.');
+            if (index != -1) {
+                coordonates.put("extension", artifactUrl.substring(index + 1));
+            } else {
+                coordonates.put("extension", "jar");
+            }
+        }
+
+        File artifactFile = File.createTempFile(artifactId, coordonates.get("extension"));
+
+        FileOutputStream os = new FileOutputStream(artifactFile);
+        copyStream(new URI(artifactUrl).toURL().openStream(), os);
+        os.flush();
+        os.close();
+
+        uploadArtifact(groupId, artifactId, version, coordonates.get("extension"), artifactFile, repositoryUrl);
+    }
+
+    protected void uploadArtifact(String groupId, String artifactId, String version, String extension, File artifactFile, String repositoryUrl) throws Exception {
+        uploadArtifact(groupId, artifactId, version, extension, null, artifactFile, repositoryUrl);
+    }
+
+    protected void uploadArtifact(String groupId, String artifactId, String version, String extension, String classifier, File artifactFile, String repositoryUrl) throws Exception {
+        DefaultServiceLocator defaultServiceLocator = MavenRepositorySystemUtils.newServiceLocator();
+        defaultServiceLocator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class);
+        defaultServiceLocator.addService(TransporterFactory.class, FileTransporterFactory.class);
+        defaultServiceLocator.addService(TransporterFactory.class, HttpTransporterFactory.class);
+
+        RepositorySystem repositorySystem = defaultServiceLocator.getService(RepositorySystem.class);
+
+        DefaultRepositorySystemSession repositorySystemSession = MavenRepositorySystemUtils.newSession();
+        LocalRepository localRepository = new LocalRepository(System.getProperty("user.home") + "/.m2/repository");
+        LocalRepositoryManager localRepositoryManager = repositorySystem.newLocalRepositoryManager(repositorySystemSession, localRepository);
+        repositorySystemSession.setLocalRepositoryManager(localRepositoryManager);
+        repositorySystemSession.setTransferListener(new ConsoleTransferListener(System.out));
+        repositorySystemSession.setRepositoryListener(new ConsoleRepositoryListener(System.out));
+
+        RemoteRepository remoteRepository = new RemoteRepository.Builder("sdeployer", "default", repositoryUrl).build();
+
+        Artifact artifact;
+        if (classifier != null) {
+            artifact = new DefaultArtifact(groupId, artifactId, classifier, extension, version);
+        } else {
+            artifact = new DefaultArtifact(groupId, artifactId, extension, version);
+        }
+        artifact = artifact.setFile(artifactFile);
+
+        InstallRequest installRequest = new InstallRequest();
+        installRequest.addArtifact(artifact);
+        repositorySystem.install(repositorySystemSession, installRequest);
+
+        DeployRequest deployRequest = new DeployRequest();
+        deployRequest.addArtifact(artifact);
+        deployRequest.setRepository(remoteRepository);
+        repositorySystem.deploy(repositorySystemSession, deployRequest);
+    }
+
+    @Override
+    public void assembleFeature(String groupId,
+                                String artifactId,
+                                String version,
+                                String repositoryUrl,
+                                String feature,
+                                List<String> featuresRepositoryUrls,
+                                List<String> features,
+                                List<String> bundles,
+                                List<org.apache.karaf.cave.deployer.api.Config> configs) throws Exception {
+        Features featuresModel = new Features();
+        featuresModel.setName(feature);
+        // add features repository
+        if (featuresRepositoryUrls != null) {
+            for (String featuresRepositoryUrl : featuresRepositoryUrls) {
+                featuresModel.getRepository().add(featuresRepositoryUrl);
+            }
+        }
+        // add wrap feature
+        Feature wrapFeature = new Feature();
+        wrapFeature.setName(feature);
+        wrapFeature.setVersion(version);
+        // add inner features
+        if (features != null) {
+            for (String innerFeature : features) {
+                Dependency dependency = new Dependency();
+                dependency.setName(innerFeature);
+                wrapFeature.getFeature().add(dependency);
+            }
+        }
+        // add bundles
+        if (bundles != null) {
+            for (String innerBundle : bundles) {
+                Bundle bundle = new Bundle();
+                bundle.setLocation(innerBundle);
+                wrapFeature.getBundle().add(bundle);
+            }
+        }
+        // add config
+        if (configs != null) {
+            for (org.apache.karaf.cave.deployer.api.Config config : configs) {
+                Config modelConfig = new Config();
+                modelConfig.setName(config.getPid());
+                StringBuilder builder = new StringBuilder();
+                if (config.getProperties() != null) {
+                    for (String key : config.getProperties().keySet()) {
+                        builder.append(key).append("=").append(config.getProperties().get(key)).append('\n');
+                    }
+                }
+                modelConfig.setValue(builder.toString());
+                wrapFeature.getConfig().add(modelConfig);
+            }
+        }
+        featuresModel.getFeature().add(wrapFeature);
+        File featuresFile = File.createTempFile(artifactId, "xml");
+        FileOutputStream os = new FileOutputStream(featuresFile);
+        JaxbUtil.marshal(featuresModel, os);
+        uploadArtifact(groupId, artifactId, version, "xml", "features", featuresFile, repositoryUrl);
+    }
+
+    @Override
+    public void installKar(String artifactUrl, String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf:type=kar,name=" + karafName);
+            connection.invoke(name, "install", new Object[]{ artifactUrl }, new String[]{ "java.lang.String" });
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+    }
+
+    @Override
+    public void uninstallKar(String id, String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf:type=kar,name=" + karafName);
+            connection.invoke(name, "uninstall", new Object[]{id}, new String[]{ "java.lang.String" });
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+    }
+
+    @Override
+    public List<String> listKars(String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf:type=kar,name=" + karafName);
+            return ((List<String>) connection.getAttribute(name, "Kars"));
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+    }
+
+    @Override
+    public void deployBundle(String artifactUrl, String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf:type=bundle,name=" + karafName);
+            connection.invoke(name, "install", new Object[]{ artifactUrl, true }, new String[]{ "java.lang.String", boolean.class.getName() });
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+    }
+
+    @Override
+    public void undeployBundle(String id, String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf:type=bundle,name=" + karafName);
+            connection.invoke(name, "uninstall", new Object[]{id}, new String[]{ "java.lang.String" });
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+    }
+
+    @Override
+    public void startBundle(String id, String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf:type=bundle,name=" + karafName);
+            connection.invoke(name, "start", new Object[]{id}, new String[]{ "java.lang.String" });
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+    }
+
+    @Override
+    public void stopBundle(String id, String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf:type=bundle,name=" + karafName);
+            connection.invoke(name, "stop", new Object[]{id}, new String[]{ "java.lang.String" });
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+    }
+
+    @Override
+    public List<org.apache.karaf.cave.deployer.api.Bundle> listBundles(String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf:type=bundle,name=" + karafName);
+            TabularData tabularData = (TabularData) connection.getAttribute(name, "Bundles");
+            List<org.apache.karaf.cave.deployer.api.Bundle> result = new ArrayList<org.apache.karaf.cave.deployer.api.Bundle>();
+            for (Object value : tabularData.values()) {
+                CompositeData compositeData = (CompositeData) value;
+                Long bundleId = (Long) compositeData.get("ID");
+                String bundleName = (String) compositeData.get("Name");
+                String bundleVersion = (String) compositeData.get("Version");
+                String bundleState = (String) compositeData.get("State");
+                org.apache.karaf.cave.deployer.api.Bundle bundle = new org.apache.karaf.cave.deployer.api.Bundle();
+                bundle.setId(bundleId.toString());
+                bundle.setName(bundleName);
+                bundle.setVersion(bundleVersion);
+                bundle.setState(bundleState);
+                result.add(bundle);
+            }
+            return result;
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+    }
+
+    @Override
+    public void addFeaturesRepository(String artifactUrl, String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf:type=feature,name=" + karafName);
+            connection.invoke(name, "addRepository", new Object[]{ artifactUrl, false }, new String[]{ "java.lang.String", boolean.class.getName() });
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+    }
+
+    @Override
+    public void removeFeaturesRepository(String artifactUrl, String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf:type=feature,name=" + karafName);
+            connection.invoke(name, "removeRepository", new Object[]{ artifactUrl, true }, new String[]{ "java.lang.String", boolean.class.getName() });
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+    }
+
+    @Override
+    public List<FeaturesRepository> listFeaturesRepositories(String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf:type=feature,name=" + karafName);
+            List<FeaturesRepository> result = new ArrayList<FeaturesRepository>();
+            TabularData tabularData = (TabularData) connection.getAttribute(name, "Repositories");
+            for (Object value : tabularData.values()) {
+                CompositeData compositeData = (CompositeData) value;
+                String repoName = (String) compositeData.get("Name");
+                String repoUri = (String) compositeData.get("Uri");
+                FeaturesRepository repo = new FeaturesRepository();
+                repo.setName(repoName);
+                repo.setUri(repoUri);
+                result.add(repo);
+            }
+            return result;
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+    }
+
+    @Override
+    public void installFeature(String feature, String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf:type=feature,name=" + karafName);
+            connection.invoke(name, "installFeature", new Object[]{ feature }, new String[]{ "java.lang.String" });
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+    }
+
+    @Override
+    public void uninstallFeature(String feature, String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf:type=feature,name=" + karafName);
+            connection.invoke(name, "uninstallFeature", new Object[]{ feature }, new String[]{ "java.lang.String", });
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+    }
+
+    @Override
+    public List<org.apache.karaf.cave.deployer.api.Feature> listFeatures(String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf:type=feature,name=" + karafName);
+            TabularData tabularData = (TabularData) connection.getAttribute(name, "Features");
+            List<org.apache.karaf.cave.deployer.api.Feature> result = new ArrayList<>();
+            for (Object value : tabularData.values()) {
+                CompositeData compositeData = (CompositeData) value;
+                String featureName = (String) compositeData.get("Name");
+                String featureVersion = (String) compositeData.get("Version");
+                boolean featureInstalled = (Boolean) compositeData.get("Installed");
+                org.apache.karaf.cave.deployer.api.Feature feature = new org.apache.karaf.cave.deployer.api.Feature();
+                feature.setName(featureName);
+                feature.setVersion(featureVersion);
+                if (featureInstalled)
+                    feature.setState("Installed");
+                else feature.setState("Uninstalled");
+                result.add(feature);
+            }
+            return result;
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+    }
+
+    @Override
+    public List<String> listInstalledFeatures(String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf:type=feature,name=" + karafName);
+            TabularData tabularData = (TabularData) connection.getAttribute(name, "Features");
+            List<String> result = new ArrayList<String>();
+            for (Object value : tabularData.values()) {
+                CompositeData compositeData = (CompositeData) value;
+                String featureName = (String) compositeData.get("Name");
+                String featureVersion = (String) compositeData.get("Version");
+                boolean featureInstalled = (Boolean) compositeData.get("Installed");
+                if (featureInstalled)
+                    result.add(featureName + "/" + featureVersion);
+            }
+            return result;
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+    }
+
+    @Override
+    public void createConfig(String pid, String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf:type=config,name=" + karafName);
+            connection.invoke(name, "create", new Object[]{ pid }, new String[]{ String.class.getName() });
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+    }
+
+    @Override
+    public void deleteConfig(String pid, String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf:type=config,name=" + karafName);
+            connection.invoke(name, "delete", new Object[]{ pid }, new String[]{ String.class.getName() });
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+    }
+
+    @Override
+    public void setConfigProperty(String pid, String key, String value, String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf:type=config,name=" + karafName);
+            connection.invoke(name, "setProperty", new Object[]{ pid, key, value }, new String[]{ String.class.getName(), String.class.getName(), String.class.getName() });
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+    }
+
+    @Override
+    public String getConfigProperty(String pid, String key, String jmxUrl, String karafName, String user, String password) throws Exception {
+        Map<String, String> properties = this.getConfigProperties(pid, jmxUrl, karafName, user, password);
+        return properties.get(key);
+    }
+
+    @Override
+    public void deleteConfigProperty(String pid, String key, String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf:type=config,name=" + karafName);
+            connection.invoke(name, "deleteProperty", new Object[]{ pid, key }, new String[]{ String.class.getName(), String.class.getName()} );
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+    }
+
+    @Override
+    public void appendConfigProperty(String pid, String key, String value, String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf:type=config,name=" + karafName);
+            connection.invoke(name, "appendProperty", new Object[]{ pid, key, value }, new String[]{ String.class.getName(), String.class.getName(), String.class.getName() });
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+    }
+
+    @Override
+    public void updateConfig(org.apache.karaf.cave.deployer.api.Config config, String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf:type=config,name=" + karafName);
+            connection.invoke(name, "update", new Object[] { config.getPid(), config.getProperties() }, new String[]{ String.class.getName(), Map.class.getName() });
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+    }
+
+    @Override
+    public Map<String, String> getConfigProperties(String pid, String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf:type=config,name=" + karafName);
+            Map<String, String> result = (Map<String, String>) connection.invoke(name, "listProperties", new Object[]{ pid }, new String[]{ String.class.getName() });
+            return result;
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+    }
+
+    @Override
+    public List<String> clusterNodes(String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        List<String> nodes = new ArrayList<String>();
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf.cellar:type=node,name=" + karafName);
+            TabularData tabularData = (TabularData) connection.getAttribute(name, "nodes");
+            for (Object value : tabularData.values()) {
+                CompositeData data = (CompositeData) value;
+                String id = (String) data.get("id");
+                nodes.add(id);
+            }
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+        return nodes;
+    }
+
+    @Override
+    public Map<String, List<String>> clusterGroups(String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        Map<String, List<String>> groups = new HashMap<String, List<String>>();
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf.cellar:type=group,name=" + karafName);
+            TabularData tabularData = (TabularData) connection.getAttribute(name, "groups");
+            for (Object value : tabularData.values()) {
+                CompositeData data = (CompositeData) value;
+                String group = (String) data.get("name");
+                String members = (String) data.get("members");
+                List<String> m = Arrays.asList(members.split(" "));
+                groups.put(group, m);
+            }
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+        return groups;
+    }
+
+    @Override
+    public void clusterFeatureInstall(String feature, String clusterGroup, String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf.cellar:type=feature,name=" + karafName);
+            connection.invoke(name, "installFeature", new Object[]{ clusterGroup, feature }, new String[]{ "java.lang.String", "java.lang.String"});
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+    }
+
+    @Override
+    public boolean isFeatureOnClusterGroup(String feature, String clusterGroup, String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf.cellar:type=feature,name=" + karafName);
+            TabularData tabularData = (TabularData) connection.getAttribute(name, "features");
+            for (Object value : tabularData.values()) {
+                CompositeData data = (CompositeData) value;
+                String featureName = (String) data.get("name");
+                boolean installed = (Boolean) data.get("installed");
+                if (feature.equals(featureName) && installed) {
+                    return true;
+                }
+            }
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean isFeatureLocal(String feature, String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf:type=feature,name=" + karafName);
+            TabularData tabularData = (TabularData) connection.getAttribute(name, "features");
+            for (Object value : tabularData.values()) {
+                CompositeData data = (CompositeData) value;
+                String featureName = (String) data.get("name");
+                boolean installed = (Boolean) data.get("installed");
+                if (feature.equals(featureName) && installed) {
+                    return true;
+                }
+            }
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public void clusterRemoveFeaturesRepository(String id, String clusterGroup, String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf.cellar:type=feature,name=" + karafName);
+            connection.invoke(name, "removeRepository", new Object[]{ clusterGroup, id }, new String[]{ "java.lang.String", "java.lang.String" });
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+    }
+
+    @Override
+    public boolean isFeaturesRepositoryLocal(String id, String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf:type=feature,name=" + karafName);
+            TabularData tabularData = (TabularData) connection.getAttribute(name, "repositories");
+            for (Object value : tabularData.values()) {
+                CompositeData data = (CompositeData) value;
+                String repoName = (String) data.get("Name");
+                String url = (String) data.get("Uri");
+                if (repoName.equals(id) || url.equals(id)) {
+                    return true;
+                }
+            }
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public void clusterFeatureUninstall(String feature, String clusterGroup, String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf.cellar:type=feature,name=" + karafName);
+            connection.invoke(name, "uninstallFeature", new Object[]{ clusterGroup, feature }, new String[]{ "java.lang.String", "java.lang.String"});
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+    }
+
+    @Override
+    public void clusterAddFeaturesRepository(String url, String clusterGroup, String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf.cellar:type=feature,name=" + karafName);
+            connection.invoke(name, "addRepository", new Object[]{ clusterGroup, url }, new String[]{ "java.lang.String", "java.lang.String" });
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+    }
+
+    @Override
+    public boolean isFeaturesRepositoryOnClusterGroup(String id, String clusterGroup, String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXConnector jmxConnector = connect(jmxUrl, karafName, user, password);
+        try {
+            MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+            ObjectName name = new ObjectName("org.apache.karaf.cellar:type=feature,name=" + karafName);
+            List<String> repositories = (List<String>) connection.getAttribute(name, "repositories");
+            for (String repository : repositories) {
+                if (repository.equals("id")) {
+                    return true;
+                }
+            }
+        } finally {
+            if (jmxConnector != null) {
+                jmxConnector.close();
+            }
+        }
+        return false;
+    }
+
+    private JMXConnector connect(String jmxUrl, String karafName, String user, String password) throws Exception {
+        JMXServiceURL jmxServiceURL = new JMXServiceURL(jmxUrl);
+        Hashtable<String, Object> env = new Hashtable<String, Object>();
+        String[] credentials = new String[]{ user, password };
+        env.put("jmx.remote.credentials", credentials);
+        return JMXConnectorFactory.connect(jmxServiceURL, env);
+    }
+
+}
diff --git a/deployer/service/src/test/java/org/apache/karaf/cave/deployer/service/impl/DeployerImplTest.java b/deployer/service/src/test/java/org/apache/karaf/cave/deployer/service/impl/DeployerImplTest.java
new file mode 100644
index 0000000..afc71ce
--- /dev/null
+++ b/deployer/service/src/test/java/org/apache/karaf/cave/deployer/service/impl/DeployerImplTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.karaf.cave.deployer.service.impl;
+
+import org.apache.karaf.cave.deployer.api.Config;
+import org.apache.karaf.cave.deployer.api.Deployer;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class DeployerImplTest {
+
+    private Deployer deployer;
+
+    @Before
+    public void startup() {
+        System.setProperty("java.protocol.handler.pkgs", "org.ops4j.pax.url");
+        System.setProperty("java.io.tmpdir", "target");
+        deployer = new DeployerImpl();
+    }
+
+    @Test
+    public void downloadArtifactTest() throws Exception {
+        deployer.downloadArtifact("mvn:commons-lang/commons-lang/2.6", "target/test/commons-lang-2.6.jar");
+    }
+
+    @Test
+    public void explodeKarTest() throws Exception {
+        deployer.explodeKar("mvn:org.apache.karaf.features/framework/4.1.3/kar", "file:target/test/repository/kar");
+    }
+
+    @Test
+    public void uploadArtifactTest() throws Exception {
+        deployer.uploadArtifact("test", "test", "1.0-SNAPSHOT", "mvn:commons-lang/commons-lang/2.6", "file:target/test/repository");
+    }
+
+    @Test
+    public void assembleFeatureTest() throws Exception {
+        List<String> featureRepositories = new ArrayList<String>();
+        featureRepositories.add("mvn:org.apache.camel.karaf/apache-camel/2.17.2/xml/features");
+        List<String> features = new ArrayList<String>();
+        features.add("camel-spring");
+        features.add("camel-jms");
+        features.add("camel-stream");
+        List<String> bundles = new ArrayList<String>();
+        bundles.add("mvn:commons-lang/commons-lang/2.6");
+        deployer.assembleFeature("test-feature", "test-feature", "1.0-SNAPSHOT", "file:target/test/repository", "test-feature", featureRepositories, features, bundles, null);
+    }
+
+    @Test
+    public void assembleFeatureWithConfigTest() throws Exception {
+        List<String> features = new ArrayList<String>();
+        features.add("eventadmin");
+        List<Config> configs = new ArrayList<Config>();
+        Config config = new Config();
+        config.setPid("org.mytest");
+        config.getProperties().put("foo", "bar");
+        config.getProperties().put("other", "value");
+        configs.add(config);
+        deployer.assembleFeature("config-feature", "config-feature", "1.0-SNAPSHOT", "file:target/test/repository", "config-feature", null, features, null, configs);
+    }
+
+    @Test
+    public void assembleFeatureWithNullTest() throws Exception {
+        deployer.assembleFeature("null-feature", "null-feature", "1.0-SNAPSHOT", "file:target/test/repository", "null-feature", null, null, null, null);
+    }
+
+    @Test
+    public void mvnParseTest() throws Exception {
+        String mvnUrl = "mvn:testGroupId/testArtifactId/1.0";
+        Map<String, String> coordonates = DeployerImpl.parse(mvnUrl);
+        Assert.assertEquals("testGroupId", coordonates.get("groupId"));
+        Assert.assertEquals("testArtifactId", coordonates.get("artifactId"));
+        Assert.assertEquals("1.0", coordonates.get("version"));
+        Assert.assertEquals("jar", coordonates.get("extension"));
+        Assert.assertNull(coordonates.get("classifier"));
+
+        mvnUrl = "mvn:testGroupId/testArtifactId/1.0/kar";
+        coordonates = DeployerImpl.parse(mvnUrl);
+        Assert.assertEquals("testGroupId", coordonates.get("groupId"));
+        Assert.assertEquals("testArtifactId", coordonates.get("artifactId"));
+        Assert.assertEquals("1.0", coordonates.get("version"));
+        Assert.assertEquals("kar", coordonates.get("extension"));
+        Assert.assertNull(coordonates.get("classifier"));
+
+        mvnUrl = "mvn:testGroupId/testArtifactId/1.0/xml/features";
+        coordonates = DeployerImpl.parse(mvnUrl);
+        Assert.assertEquals("testGroupId", coordonates.get("groupId"));
+        Assert.assertEquals("testArtifactId", coordonates.get("artifactId"));
+        Assert.assertEquals("1.0", coordonates.get("version"));
+        Assert.assertEquals("xml", coordonates.get("extension"));
+        Assert.assertEquals("features", coordonates.get("classifier"));
+    }
+
+}
diff --git a/pom.xml b/pom.xml
index 83dc8ab..60b9d9e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -54,6 +54,7 @@
     <modules>
         <module>server</module>
         <!-- <module>client</module> -->
+        <module>deployer</module>
         <module>assembly</module>
     </modules>
 
@@ -220,7 +221,7 @@
                 <plugin>
                     <groupId>org.apache.felix</groupId>
                     <artifactId>maven-bundle-plugin</artifactId>
-                    <version>2.5.4</version>
+                    <version>3.3.0</version>
                     <extensions>true</extensions>
                     <configuration>
                         <instructions>


 

----------------------------------------------------------------
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


> Add deployer feature
> --------------------
>
>                 Key: KARAF-5108
>                 URL: https://issues.apache.org/jira/browse/KARAF-5108
>             Project: Karaf
>          Issue Type: New Feature
>          Components: cave-deployer
>            Reporter: Jean-Baptiste Onofré
>            Assignee: Jean-Baptiste Onofré
>             Fix For: cave-4.1.0
>
>




--
This message was sent by Atlassian JIRA
(v6.4.14#64029)

Mime
View raw message