third_party: initial checkin of kubernetes Jenkins plugin.

Change-Id: If02a3eca2ac0dc2bff70fd5d8f1f25150af1e370
diff --git a/java/jenkins_plugins/kubernetes/.gitignore b/java/jenkins_plugins/kubernetes/.gitignore
new file mode 100644
index 0000000..14a7000
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/.gitignore
@@ -0,0 +1,13 @@
+# IntelliJ
+.idea
+*.iml
+
+# Eclipse
+.classpath
+.project
+.settings/
+
+target
+work*
+
+node_modules
diff --git a/java/jenkins_plugins/kubernetes/Dockerfile b/java/jenkins_plugins/kubernetes/Dockerfile
new file mode 100644
index 0000000..148cb4e
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/Dockerfile
@@ -0,0 +1,14 @@
+FROM csanchez/jenkins-for-volumes:1.625.1
+
+COPY src/main/docker/plugins.txt /usr/share/jenkins/plugins.txt
+RUN /usr/local/bin/plugins.sh /usr/share/jenkins/plugins.txt
+
+# ENV VERSION 0.4-SNAPSHOT
+# COPY target/kubernetes.hpi /usr/share/jenkins/ref/plugins/kubernetes.hpi
+# RUN curl -o /usr/share/jenkins/ref/plugins/kubernetes.hpi \
+#  http://repo.jenkins-ci.org/snapshots/org/csanchez/jenkins/plugins/kubernetes/0.4/kubernetes-$VERSION.hpi
+
+# remove executors in master
+COPY src/main/docker/master-executors.groovy /usr/share/jenkins/ref/init.groovy.d/
+
+# ENV JAVA_OPTS="-Djava.util.logging.config.file=/var/jenkins_home/log.properties"
diff --git a/java/jenkins_plugins/kubernetes/LICENSE b/java/jenkins_plugins/kubernetes/LICENSE
new file mode 100644
index 0000000..e06d208
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/LICENSE
@@ -0,0 +1,202 @@
+Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright {yyyy} {name of copyright owner}
+
+   Licensed 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.
+
diff --git a/java/jenkins_plugins/kubernetes/README b/java/jenkins_plugins/kubernetes/README
new file mode 100644
index 0000000..d804cd9
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/README
@@ -0,0 +1,7 @@
+To build and use the plugin:
+- Run: mvn install
+- Upload target/vanadium_kubernetes.hpi to Jenkins using "Upload Plugin" at:
+  http://dev.v.io/jenkins/pluginManager/advanced.
+- Check the "restart Jenkins" checkbox. Jenkins will wait until there are
+  no pending jobs before restarting, this might take some time during busy
+  periods.
diff --git a/java/jenkins_plugins/kubernetes/README.google b/java/jenkins_plugins/kubernetes/README.google
new file mode 100644
index 0000000..8b55f53
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/README.google
@@ -0,0 +1,10 @@
+URL: https://github.com/jenkinsci/kubernetes-plugin/archive/28507c1a8ca34969fe8010079be88db344cac239.zip
+Version: 28507c1a8ca34969fe8010079be88db344cac239
+License: Apache 2.0, MIT
+License File: LICENSE
+
+Description:
+Run dynamic slaves in a Kubernetes/Docker environment.
+
+Local Modifications:
+None.
diff --git a/java/jenkins_plugins/kubernetes/README.md b/java/jenkins_plugins/kubernetes/README.md
new file mode 100644
index 0000000..f2c78b6
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/README.md
@@ -0,0 +1,139 @@
+jenkins-kubernetes-plugin
+=========================
+
+Jenkins plugin to run dynamic slaves in a Kubernetes/Docker environment.
+
+Based on the [Scaling Docker with Kubernetes](http://www.infoq.com/articles/scaling-docker-with-kubernetes) article,
+automates the scaling of Jenkins slaves running in Kubernetes.
+
+The plugin creates a Kubernetes Pod for each slave started,
+defined by the Docker image to run, and stops it after each build.
+
+Slaves are launched using JNLP, so it is expected that the image connects automatically to the Jenkins master.
+For that some environment variables are automatically injected:
+
+* `JENKINS_URL`: Jenkins web interface url
+* `JENKINS_JNLP_URL`: url for the jnlp definition of the specific slave
+* `JENKINS_SECRET`: the secret key for authentication
+
+Tested with [`csanchez/jenkins-slave`](https://registry.hub.docker.com/u/csanchez/jenkins-slave/),
+see the [Docker image source code](https://github.com/carlossg/jenkins-slave-docker).
+
+
+# Configuration on Google Container Engine
+
+Create a cluster 
+```
+    gcloud container clusters create jenkins --num-nodes 1 --machine-type g1-small
+```
+and note the admin password and server certitifate.
+
+Or use Google Developer Console to create a Container Engine cluster, then run 
+```
+    gcloud container clusters get-credentials
+    kubectl config view --raw
+```
+the last command will output kubernetes cluster configuration including API server URL, admin password and root certificate
+
+# Debugging
+
+To inspect the json messages sent back and forth to the Kubernetes API server you can configure
+a new [Jenkins log recorder](https://wiki.jenkins-ci.org/display/JENKINS/Logging) for `org.apache.http`
+at `DEBUG` level.
+
+
+# Building
+
+Run `mvn clean package` and copy `target/kubernetes.hpi` to Jenkins plugins folder.
+
+# Docker image
+
+Docker image for Jenkins, with plugin installed.
+Based on the [official image](https://registry.hub.docker.com/_/jenkins/).
+
+## Running
+
+    docker run --rm --name jenkins -p 8080:8080 -p 50000:50000 -v /var/jenkins_home csanchez/jenkins-kubernetes
+
+## Testing locally
+
+A local testing cluster with one node can be created with Docker Compose
+
+    docker-compose up
+
+When using boot2docker or Docker Engine with a remote host, the remote Kubernetes API can be exposed
+with `docker-machine ssh MACHINE_NAME -L 0.0.0.0:8080:localhost:8080` or `boot2docker ssh -L 0.0.0.0:8080:localhost:8080`
+
+    kubectl create -f ./src/main/kubernetes/jenkins-local.yml
+    kubectl create -f ./src/main/kubernetes/service.yml
+
+More info
+
+* [Docker CookBook examples](https://github.com/how2dock/docbook/tree/master/ch05/docker)
+* [Kubernetes Getting started with Docker](https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/getting-started-guides/docker.md)
+
+## Running in Kubernetes (Google Container Engine)
+
+Assuming you created a Kubernetes cluster named `jenkins` this is how to run both Jenkins and slaves there.
+
+Create a GCE disk named `kubernetes-jenkins` to store the data.
+
+    gcloud compute disks create --size 20GB kubernetes-jenkins
+
+Creating the pods and services
+
+    kubectl create -f ./src/main/kubernetes/jenkins-gke.yml
+    kubectl create -f ./src/main/kubernetes/service-gke.yml
+
+Connect to the ip of the network load balancer created by Kubernetes, port 80.
+Get the ip (in this case `104.197.19.100`) with `kubectl describe services/jenkins`
+(it may take a bit to populate)
+
+    $ kubectl describe services/jenkins
+    Name:           jenkins
+    Namespace:      default
+    Labels:         <none>
+    Selector:       name=jenkins
+    Type:           LoadBalancer
+    IP:         10.175.244.232
+    LoadBalancer Ingress:   104.197.19.100
+    Port:           http    80/TCP
+    NodePort:       http    30080/TCP
+    Endpoints:      10.172.1.5:8080
+    Port:           slave   50000/TCP
+    NodePort:       slave   32081/TCP
+    Endpoints:      10.172.1.5:50000
+    Session Affinity:   None
+    No events.
+
+Configure Jenkins, adding the `Kubernetes` cloud under configuration, setting
+Kubernetes URL to the container engine cluster endpoint or simply `https://kubernetes.default.svc.cluster.local`.
+Under credentials, click `Add` and select `Kubernetes Service Account`,
+or alternatively use the Kubernetes API username and password.
+
+![image](credentials.png)
+
+You may want to set `Jenkins URL` to the internal service IP, `http://10.175.244.232` in this case,
+to connect through the internal network.
+
+Set `Container Cap` to a reasonable number for tests, i.e. 3.
+
+Add an image with
+
+* Docker image: `jenkinsci/jnlp-slave`
+* Jenkins slave root directory: `/home/jenkins`
+
+![image](configuration.png)
+
+Now it is ready to be used.
+
+Tearing it down
+
+    kubectl stop rc/jenkins
+    kubectl delete services/jenkins
+
+
+
+## Building
+
+    docker build -t csanchez/jenkins-kubernetes .
diff --git a/java/jenkins_plugins/kubernetes/configuration.png b/java/jenkins_plugins/kubernetes/configuration.png
new file mode 100644
index 0000000..3d4ca56
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/configuration.png
Binary files differ
diff --git a/java/jenkins_plugins/kubernetes/credentials.png b/java/jenkins_plugins/kubernetes/credentials.png
new file mode 100644
index 0000000..3107b4e
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/credentials.png
Binary files differ
diff --git a/java/jenkins_plugins/kubernetes/docker-compose.yml b/java/jenkins_plugins/kubernetes/docker-compose.yml
new file mode 100644
index 0000000..64afa6f
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/docker-compose.yml
@@ -0,0 +1,27 @@
+# Docker Compose definition for a one node Kubernetes cluster
+# Based on Docker Cookbook example
+# https://github.com/how2dock/docbook/ch05/docker
+etcd:
+  image: gcr.io/google_containers/etcd:2.0.12
+  net: "host"
+  command: /usr/local/bin/etcd --addr=127.0.0.1:4001 --bind-addr=0.0.0.0:4001 --data-dir=/var/etcd/data
+master:
+  image: gcr.io/google_containers/hyperkube:v1.1.3
+  net: "host"
+  pid: "host"
+  privileged: true
+  volumes:
+    - /:/rootfs:ro
+    - /sys:/sys:ro
+    - /dev:/dev
+    - /var/lib/docker/:/var/lib/docker:ro
+    - /var/lib/kubelet/:/var/lib/kubelet:rw
+    - /var/run:/var/run:rw
+    - /var/run/docker.sock:/var/run/docker.sock
+  command:  /hyperkube kubelet --containerized --hostname-override="127.0.0.1" --address="0.0.0.0" --api-servers=http://localhost:8080 --config=/etc/kubernetes/manifests
+
+proxy:
+  image: gcr.io/google_containers/hyperkube:v1.1.3
+  net: "host"
+  privileged: true
+  command: /hyperkube proxy --master=http://127.0.0.1:8080 --v=2
diff --git a/java/jenkins_plugins/kubernetes/pom.xml b/java/jenkins_plugins/kubernetes/pom.xml
new file mode 100644
index 0000000..59256a8
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/pom.xml
@@ -0,0 +1,116 @@
+<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/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.jenkins-ci.plugins</groupId>
+    <artifactId>plugin</artifactId>
+    <version>1.609.2</version>
+  </parent>
+
+  <groupId>org.csanchez.jenkins.plugins</groupId>
+  <artifactId>kubernetes</artifactId>
+  <version>0.6-SNAPSHOT</version>
+  <name>Kubernetes plugin</name>
+  <description>Jenkins plugin to run dynamic slaves in a Kubernetes/Docker environment</description>
+  <packaging>hpi</packaging>
+  <url>https://wiki.jenkins-ci.org/display/JENKINS/Kubernetes+Plugin</url>
+
+  <scm>
+    <connection>scm:git:ssh://github.com/jenkinsci/kubernetes-plugin.git</connection>
+    <developerConnection>scm:git:ssh://git@github.com/jenkinsci/kubernetes-plugin.git</developerConnection>
+    <url>https://github.com/jenkinsci/kubernetes-plugin</url>
+    <tag>HEAD</tag>
+  </scm>
+
+  <developers>
+    <developer>
+      <id>carlos</id>
+      <name>Carlos Sanchez</name>
+      <email>carlos@apache.org</email>
+    </developer>
+  </developers>
+
+  <properties>
+    <jackson.version>2.5.0</jackson.version>
+    <spring.version>3.2.14.RELEASE</spring.version>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.jenkins-ci.plugins</groupId>
+      <artifactId>credentials</artifactId>
+      <version>1.22</version>
+    </dependency>
+
+    <dependency>
+      <groupId>io.fabric8</groupId>
+      <artifactId>kubernetes-client</artifactId>
+      <version>1.3.47</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.httpcomponents</groupId>
+      <artifactId>httpclient</artifactId>
+      <version>4.5.1</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.jenkins-ci.plugins</groupId>
+      <artifactId>durable-task</artifactId>
+      <version>1.6</version>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+      <version>1.7.7</version>
+      <scope>provided</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.jenkins-ci.tools</groupId>
+        <artifactId>maven-hpi-plugin</artifactId>
+        <configuration>
+          <pluginFirstClassLoader>true</pluginFirstClassLoader>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+  <profiles>
+    <profile>
+      <id>doclint-java8-disable</id>
+      <activation>
+        <jdk>[1.8,)</jdk>
+      </activation>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-javadoc-plugin</artifactId>
+            <configuration>
+              <additionalparam>-Xdoclint:none</additionalparam>
+            </configuration>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+
+  <!-- get every artifact through repo.jenkins-ci.org, which proxies all the artifacts that we need -->
+  <repositories>
+    <repository>
+      <id>repo.jenkins-ci.org</id>
+      <url>http://repo.jenkins-ci.org/public/</url>
+    </repository>
+  </repositories>
+
+  <pluginRepositories>
+    <pluginRepository>
+      <id>repo.jenkins-ci.org</id>
+      <url>http://repo.jenkins-ci.org/public/</url>
+    </pluginRepository>
+  </pluginRepositories>
+
+</project>
diff --git a/java/jenkins_plugins/kubernetes/src/main/docker/master-executors.groovy b/java/jenkins_plugins/kubernetes/src/main/docker/master-executors.groovy
new file mode 100644
index 0000000..612824c
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/docker/master-executors.groovy
@@ -0,0 +1,5 @@
+import hudson.model.*;
+import jenkins.model.*;
+
+println "--> disabling master executors"
+Jenkins.instance.setNumExecutors(0)
diff --git a/java/jenkins_plugins/kubernetes/src/main/docker/plugins.txt b/java/jenkins_plugins/kubernetes/src/main/docker/plugins.txt
new file mode 100644
index 0000000..4fd8a1e
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/docker/plugins.txt
@@ -0,0 +1,3 @@
+durable-task:1.6
+credentials:1.22
+kubernetes:0.4.1
\ No newline at end of file
diff --git a/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/KubectlBuildWrapper.java b/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/KubectlBuildWrapper.java
new file mode 100644
index 0000000..889af0b
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/KubectlBuildWrapper.java
@@ -0,0 +1,171 @@
+package org.csanchez.jenkins.plugins.kubernetes;
+
+import com.cloudbees.plugins.credentials.CredentialsMatchers;
+import com.cloudbees.plugins.credentials.CredentialsProvider;
+import com.cloudbees.plugins.credentials.common.StandardCredentials;
+import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
+import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
+import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials;
+import com.cloudbees.plugins.credentials.domains.DomainRequirement;
+import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder;
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import hudson.AbortException;
+import hudson.EnvVars;
+import hudson.Extension;
+import hudson.FilePath;
+import hudson.Launcher;
+import hudson.model.AbstractProject;
+import hudson.model.Item;
+import hudson.model.Run;
+import hudson.model.TaskListener;
+import hudson.security.ACL;
+import hudson.tasks.BuildWrapperDescriptor;
+import hudson.util.ListBoxModel;
+import hudson.util.Secret;
+import jenkins.model.Jenkins;
+import jenkins.tasks.SimpleBuildWrapper;
+import org.apache.commons.lang.StringUtils;
+import org.kohsuke.stapler.AncestorInPath;
+import org.kohsuke.stapler.DataBoundConstructor;
+import org.kohsuke.stapler.QueryParameter;
+
+import javax.annotation.Nonnull;
+import java.io.IOException;
+import java.util.Collections;
+
+/**
+ * @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
+ */
+public class KubectlBuildWrapper extends SimpleBuildWrapper {
+
+    private final String serverUrl;
+    private final String credentialsId;
+
+    @DataBoundConstructor
+    public KubectlBuildWrapper(@Nonnull String serverUrl, @Nonnull String credentialsId) {
+        this.serverUrl = serverUrl;
+        this.credentialsId = credentialsId;
+    }
+
+    public String getServerUrl() {
+        return serverUrl;
+    }
+
+    public String getCredentialsId() {
+        return credentialsId;
+    }
+
+    @Override
+    public void setUp(Context context, Run<?, ?> build, FilePath workspace, Launcher launcher, TaskListener listener, EnvVars initialEnvironment) throws IOException, InterruptedException {
+
+        FilePath configFile = workspace.createTempFile(".kube", "config");
+
+        int status = launcher.launch()
+                .cmdAsSingleString("kubectl config --kubeconfig=" + configFile.getRemote() + " set-cluster k8s --server=" + serverUrl + " --insecure-skip-tls-verify=true")
+                .join();
+        if (status != 0) throw new IOException("Failed to run kubectl config "+status);
+
+        final StandardCredentials c = getCredentials();
+
+        String login;
+        if (c == null) {
+            throw new AbortException("No credentials defined to setup Kubernetes CLI");
+        } else if (c instanceof TokenProducer) {
+            login = "--token=" + ((TokenProducer) c).getToken(serverUrl, null, true);
+        } else if (c instanceof UsernamePasswordCredentials) {
+            UsernamePasswordCredentials upc = (UsernamePasswordCredentials) c;
+            login = "--username=" + upc.getUsername() + " --password=" + Secret.toString(upc.getPassword());
+        } else {
+            throw new AbortException("Unsupported Credentials type " + c.getClass().getName());
+        }
+
+        status = launcher.launch()
+                .cmdAsSingleString("kubectl config --kubeconfig=" + configFile.getRemote() + " set-credentials cluster-admin " + login)
+                .masks(false, false, false, false, false, false, true)
+                .join();
+        if (status != 0) throw new IOException("Failed to run kubectl config "+status);
+
+        status = launcher.launch()
+                .cmdAsSingleString("kubectl config --kubeconfig=" + configFile.getRemote() + " set-context k8s --cluster=k8s --user=cluster-admin")
+                .join();
+        if (status != 0) throw new IOException("Failed to run kubectl config "+status);
+
+        status = launcher.launch()
+                .cmdAsSingleString("kubectl config --kubeconfig=" + configFile.getRemote() + " use-context k8s")
+                .join();
+        if (status != 0) throw new IOException("Failed to run kubectl config "+status);
+
+        context.setDisposer(new CleanupDisposer(configFile.getRemote()));
+
+        context.env("KUBECONFIG", configFile.getRemote());
+    }
+
+    /**
+     * Get the {@link StandardCredentials}.
+     *
+     * @return the credentials matching the {@link #credentialsId} or {@code null} is {@code #credentialsId} is blank
+     * @throws AbortException if no {@link StandardCredentials} matching {@link #credentialsId} is found
+     */
+    @CheckForNull
+    private StandardCredentials getCredentials() throws AbortException {
+        if (StringUtils.isBlank(credentialsId)) {
+            return null;
+        }
+        StandardCredentials result = CredentialsMatchers.firstOrNull(
+                CredentialsProvider.lookupCredentials(StandardCredentials.class,
+                        Jenkins.getInstance(), ACL.SYSTEM, Collections.<DomainRequirement>emptyList()),
+                CredentialsMatchers.withId(credentialsId)
+        );
+        if (result == null) {
+            throw new AbortException("No credentials found for id \"" + credentialsId + "\"");
+        }
+        return result;
+    }
+
+    @Extension
+    public static class DescriptorImpl extends BuildWrapperDescriptor {
+
+        @Override
+        public boolean isApplicable(AbstractProject<?, ?> item) {
+            return true;
+        }
+
+        @Override
+        public String getDisplayName() {
+            return "Setup Kubernetes CLI (kubectl)";
+        }
+
+        public ListBoxModel doFillCredentialsIdItems(@AncestorInPath Item item, @QueryParameter String serverUrl) {
+            return new StandardListBoxModel()
+                    .withEmptySelection()
+                    .withMatching(
+                            CredentialsMatchers.anyOf(
+                                    CredentialsMatchers.instanceOf(StandardUsernamePasswordCredentials.class),
+                                    CredentialsMatchers.instanceOf(TokenProducer.class)
+                            ),
+                            CredentialsProvider.lookupCredentials(
+                                    StandardCredentials.class,
+                                    item,
+                                    null,
+                                    URIRequirementBuilder.fromUri(serverUrl).build()
+                            )
+                    );
+
+        }
+
+    }
+
+    private static class CleanupDisposer extends Disposer {
+
+        private String configFile;
+
+        public CleanupDisposer(String configFile) {
+            this.configFile = configFile;
+        }
+
+        @Override
+        public void tearDown(Run<?, ?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException {
+            workspace.child(configFile).delete();
+        }
+    }
+}
diff --git a/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud.java b/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud.java
new file mode 100644
index 0000000..e27bc10
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud.java
@@ -0,0 +1,528 @@
+package org.csanchez.jenkins.plugins.kubernetes;
+
+import com.cloudbees.plugins.credentials.CredentialsMatchers;
+import com.cloudbees.plugins.credentials.CredentialsProvider;
+import com.cloudbees.plugins.credentials.common.StandardCredentials;
+import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
+import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
+import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import hudson.Extension;
+import hudson.Util;
+import hudson.model.Computer;
+import hudson.model.Descriptor;
+import hudson.model.Label;
+import hudson.model.Node;
+import hudson.security.ACL;
+import hudson.slaves.Cloud;
+import hudson.slaves.NodeProvisioner;
+import hudson.util.FormValidation;
+import hudson.util.ListBoxModel;
+import io.fabric8.kubernetes.api.model.*;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import jenkins.model.Jenkins;
+import jenkins.model.JenkinsLocationConfiguration;
+import org.apache.commons.lang.StringUtils;
+import org.kohsuke.stapler.DataBoundConstructor;
+import org.kohsuke.stapler.DataBoundSetter;
+import org.kohsuke.stapler.QueryParameter;
+
+import javax.annotation.CheckForNull;
+import java.io.IOException;
+import java.net.URL;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Kubernetes cloud provider.
+ * 
+ * Starts slaves in a Kubernetes cluster using defined Docker templates for each label.
+ * 
+ * @author Carlos Sanchez carlos@apache.org
+ */
+public class KubernetesCloud extends Cloud {
+
+    private static final Logger LOGGER = Logger.getLogger(KubernetesCloud.class.getName());
+    private static final Pattern SPLIT_IN_SPACES = Pattern.compile("([^\"]\\S*|\".+?\")\\s*");
+
+    private static final String DEFAULT_ID = "jenkins-slave-default";
+
+    /** label for all pods started by the plugin */
+    private static final Map<String, String> POD_LABEL = ImmutableMap.of("jenkins", "slave");
+
+    private static final String CONTAINER_NAME = "slave";
+
+    /** Default timeout for idle workers that don't correctly indicate exit. */
+    private static final int DEFAULT_RETENTION_TIMEOUT_MINUTES = 5;
+
+    private final List<PodTemplate> templates;
+    private final String serverUrl;
+    @CheckForNull
+    private String serverCertificate;
+
+    private boolean skipTlsVerify;
+
+    private String namespace;
+    private final String jenkinsUrl;
+    @CheckForNull
+    private String jenkinsTunnel;
+    @CheckForNull
+    private String credentialsId;
+    private final int containerCap;
+    private final int retentionTimeout;
+
+    private transient KubernetesClient client;
+
+    @DataBoundConstructor
+    public KubernetesCloud(String name, List<? extends PodTemplate> templates, String serverUrl, String namespace,
+            String jenkinsUrl, String containerCapStr, int connectTimeout, int readTimeout, int retentionTimeout) {
+        super(name);
+
+        Preconditions.checkArgument(!StringUtils.isBlank(serverUrl));
+
+        this.serverUrl = serverUrl;
+        this.namespace = namespace;
+        this.jenkinsUrl = jenkinsUrl;
+        if (templates != null)
+            this.templates = new ArrayList<PodTemplate>(templates);
+        else
+            this.templates = new ArrayList<PodTemplate>();
+
+        if (containerCapStr.equals("")) {
+            this.containerCap = Integer.MAX_VALUE;
+        } else {
+            this.containerCap = Integer.parseInt(containerCapStr);
+        }
+
+        if (retentionTimeout > 0) {
+            this.retentionTimeout = retentionTimeout;
+        } else {
+            this.retentionTimeout = DEFAULT_RETENTION_TIMEOUT_MINUTES;
+        }
+    }
+
+    public int getRetentionTimeout() {
+        return retentionTimeout;
+    }
+
+    public List<PodTemplate> getTemplates() {
+        return templates;
+    }
+
+    public String getServerUrl() {
+        return serverUrl;
+    }
+
+    public String getServerCertificate() {
+        return serverCertificate;
+    }
+
+    @DataBoundSetter
+    public void setServerCertificate(String serverCertificate) {
+        this.serverCertificate = Util.fixEmpty(serverCertificate);
+    }
+
+    public boolean isSkipTlsVerify() {
+        return skipTlsVerify;
+    }
+
+    @DataBoundSetter
+    public void setSkipTlsVerify(boolean skipTlsVerify) {
+        this.skipTlsVerify = skipTlsVerify;
+    }
+
+    public String getNamespace() {
+        return namespace;
+    }
+
+    public String getJenkinsUrl() {
+        return jenkinsUrl;
+    }
+
+    public String getJenkinsTunnel() {
+        return jenkinsTunnel;
+    }
+
+    @DataBoundSetter
+    public void setJenkinsTunnel(String jenkinsTunnel) {
+        this.jenkinsTunnel = Util.fixEmpty(jenkinsTunnel);
+    }
+
+    public String getCredentialsId() {
+        return credentialsId;
+    }
+
+    @DataBoundSetter
+    public void setCredentialsId(String credentialsId) {
+        this.credentialsId = Util.fixEmpty(credentialsId);
+    }
+
+    public String getContainerCapStr() {
+        if (containerCap == Integer.MAX_VALUE) {
+            return "";
+        } else {
+            return String.valueOf(containerCap);
+        }
+    }
+
+    /**
+     * Connects to Docker.
+     *
+     * @return Docker client.
+     */
+    public KubernetesClient connect() throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, IOException {
+
+        LOGGER.log(Level.FINE, "Building connection to Kubernetes host " + name + " URL " + serverUrl);
+
+        if (client == null) {
+            synchronized (this) {
+                if (client != null)
+                    return client;
+
+                client = new KubernetesFactoryAdapter(serverUrl, serverCertificate, credentialsId, skipTlsVerify)
+                        .createClient();
+            }
+        }
+        return client;
+
+    }
+
+    private String getIdForLabel(Label label) {
+        if (label == null) {
+            return DEFAULT_ID;
+        }
+        return "jenkins-" + label.getName();
+    }
+
+    private Pod getPodTemplate(KubernetesSlave slave, Label label) {
+        final PodTemplate template = getTemplate(label);
+        String id = getIdForLabel(label);
+        List<EnvVar> env = new ArrayList<EnvVar>(3);
+        // always add some env vars
+        env.add(new EnvVar("JENKINS_SECRET", slave.getComputer().getJnlpMac(), null));
+        env.add(new EnvVar("JENKINS_LOCATION_URL", JenkinsLocationConfiguration.get().getUrl(), null));
+        String url = StringUtils.isBlank(jenkinsUrl) ? JenkinsLocationConfiguration.get().getUrl() : jenkinsUrl;
+        env.add(new EnvVar("JENKINS_URL", url, null));
+        if (!StringUtils.isBlank(jenkinsTunnel)) {
+            env.add(new EnvVar("JENKINS_TUNNEL", jenkinsTunnel, null));
+        }
+        url = url.endsWith("/") ? url : url + "/";
+        env.add(new EnvVar("JENKINS_JNLP_URL", url + slave.getComputer().getUrl() + "slave-agent.jnlp", null));
+
+        // Build volumes and volume mounts.
+        List<Volume> volumes = new ArrayList<Volume>();
+        List<VolumeMount> volumeMounts = new ArrayList<VolumeMount>();
+        {
+            int i = 0;
+            for (final PodVolumes.PodVolume volume : template.getVolumes()) {
+                final String volumeName = "volume-" + i;
+                volumes.add(volume.buildVolume(volumeName));
+                volumeMounts.add(new VolumeMount(volume.getMountPath(), volumeName, false));
+                i++;
+            }
+        }
+
+        return new PodBuilder()
+                .withNewMetadata()
+                    .withName(slave.getNodeName())
+                    .withLabels(getLabelsFor(id))
+                .endMetadata()
+                .withNewSpec()
+                    .withVolumes(volumes)
+                    .addNewContainer()
+                        .withName(CONTAINER_NAME)
+                        .withImage(template.getImage())
+                        .withNewSecurityContext()
+                            .withPrivileged(template.isPrivileged())
+                        .endSecurityContext()
+                        .withVolumeMounts(volumeMounts)
+                        .withEnv(env)
+                        .withCommand(parseDockerCommand(template.getCommand()))
+                        .addToArgs(slave.getComputer().getJnlpMac())
+                        .addToArgs(slave.getComputer().getName())
+                .endContainer()
+                .withRestartPolicy("Never")
+                .endSpec()
+                .build();
+    }
+
+    private Map<String, String> getLabelsFor(String id) {
+        return ImmutableMap.<String, String> builder().putAll(POD_LABEL).putAll(ImmutableMap.of("name", id)).build();
+    }
+
+    /**
+     * Split a command in the parts that Docker need
+     * 
+     * @param dockerCommand
+     * @return
+     */
+    List<String> parseDockerCommand(String dockerCommand) {
+        if (dockerCommand == null || dockerCommand.isEmpty()) {
+            return null;
+        }
+        // handle quoted arguments
+        Matcher m = SPLIT_IN_SPACES.matcher(dockerCommand);
+        List<String> commands = new ArrayList<String>();
+        while (m.find()) {
+            commands.add(m.group(1).replace("\"", ""));
+        }
+        return commands;
+    }
+
+    @Override
+    public synchronized Collection<NodeProvisioner.PlannedNode> provision(final Label label, final int excessWorkload) {
+        try {
+
+            LOGGER.log(Level.INFO, "Excess workload after pending Spot instances: " + excessWorkload);
+
+            List<NodeProvisioner.PlannedNode> r = new ArrayList<NodeProvisioner.PlannedNode>();
+
+            final PodTemplate t = getTemplate(label);
+
+            for (int i = 1; i <= excessWorkload; i++) {
+                if (!addProvisionedSlave(t, label)) {
+                    break;
+                }
+
+                r.add(new NodeProvisioner.PlannedNode(t.getDisplayName(), Computer.threadPoolForRemoting
+                        .submit(new ProvisioningCallback(this, t, label)), 1));
+            }
+            return r;
+        } catch (Exception e) {
+            LOGGER.log(Level.WARNING, "Failed to count the # of live instances on Kubernetes", e);
+            return Collections.emptyList();
+        }
+    }
+
+    private class ProvisioningCallback implements Callable<Node> {
+        private final KubernetesCloud cloud;
+        private final PodTemplate t;
+        private final Label label;
+
+        public ProvisioningCallback(KubernetesCloud cloud, PodTemplate t, Label label) {
+            this.cloud = cloud;
+            this.t = t;
+            this.label = label;
+        }
+
+        public Node call() throws Exception {
+            KubernetesSlave slave = null;
+            try {
+
+                slave = new KubernetesSlave(t, getIdForLabel(label), cloud, label);
+                Jenkins.getInstance().addNode(slave);
+
+                Pod pod = getPodTemplate(slave, label);
+                // Why the hell doesn't createPod return a Pod object ?
+                pod = connect().pods().inNamespace(namespace).create(pod);
+
+                String podId = pod.getMetadata().getName();
+                LOGGER.log(Level.INFO, "Created Pod: {0}", podId);
+
+                // We need the pod to be running and connected before returning
+                // otherwise this method keeps being called multiple times
+                ImmutableList<String> validStates = ImmutableList.of("Running");
+
+                int i = 0;
+                int j = 100; // wait 600 seconds
+
+                // wait for Pod to be running
+                for (; i < j; i++) {
+                    LOGGER.log(Level.INFO, "Waiting for Pod to be scheduled ({1}/{2}): {0}", new Object[] {podId, i, j});
+                    Thread.sleep(6000);
+                    pod = connect().pods().inNamespace(namespace).withName(podId).get();
+                    if (pod == null) {
+                        throw new IllegalStateException("Pod no longer exists: " + podId);
+                    }
+                    ContainerStatus info = getContainerStatus(pod, CONTAINER_NAME);
+                    if (info != null) {
+                        if (info.getState().getWaiting() != null) {
+                            // Pod is waiting for some reason
+                            LOGGER.log(Level.INFO, "Pod is waiting {0}: {1}",
+                                    new Object[] { podId, info.getState().getWaiting() });
+                            // break;
+                        }
+                        if (info.getState().getTerminated() != null) {
+                            throw new IllegalStateException("Pod is terminated. Exit code: "
+                                    + info.getState().getTerminated().getExitCode());
+                        }
+                    }
+                    if (validStates.contains(pod.getStatus().getPhase())) {
+                        break;
+                    }
+                }
+                String status = pod.getStatus().getPhase();
+                if (!validStates.contains(status)) {
+                    throw new IllegalStateException("Container is not running after " + j + " attempts: " + status);
+                }
+
+                // now wait for slave to be online
+                for (; i < j; i++) {
+                    if (slave.getComputer() == null) {
+                        throw new IllegalStateException("Node was deleted, computer is null");
+                    }
+                    if (slave.getComputer().isOnline()) {
+                        break;
+                    }
+                    LOGGER.log(Level.INFO, "Waiting for slave to connect ({1}/{2}): {0}", new Object[] { podId,
+                            i, j });
+                    Thread.sleep(1000);
+                }
+                if (!slave.getComputer().isOnline()) {
+                    throw new IllegalStateException("Slave is not connected after " + j + " attempts: " + status);
+                }
+
+                return slave;
+            } catch (Throwable ex) {
+                LOGGER.log(Level.SEVERE, "Error in provisioning; slave={0}, template={1}", new Object[] { slave, t });
+                ex.printStackTrace();
+                throw Throwables.propagate(ex);
+            }
+        }
+    }
+
+    private ContainerStatus getContainerStatus(Pod pod, String containerName) {
+
+        for (ContainerStatus status : pod.getStatus().getContainerStatuses()) {
+            if (status.getName().equals(containerName)) return status;
+        }
+        return null;
+    }
+
+    /**
+     * Check not too many already running.
+     *
+     */
+    private boolean addProvisionedSlave(PodTemplate template, Label label) throws Exception {
+        if (containerCap == 0) {
+            return true;
+        }
+
+        KubernetesClient client = connect();
+        PodList slaveList = client.pods().inNamespace(namespace).withLabels(POD_LABEL).list();
+        PodList namedList = client.pods().inNamespace(namespace).withLabel("name", getIdForLabel(label)).list();
+
+
+        if (containerCap < slaveList.getItems().size()) {
+            LOGGER.log(Level.INFO, "Total container cap of " + containerCap + " reached, not provisioning.");
+            return false;
+        }
+
+        if (template.getInstanceCap() < namedList.getItems().size()) {
+            LOGGER.log(Level.INFO, "Template instance cap of " + template.getInstanceCap() + " reached for template "
+                    + template.getImage() + ", not provisioning.");
+            return false; // maxed out
+        }
+        return true;
+    }
+
+    @Override
+    public boolean canProvision(Label label) {
+        return getTemplate(label) != null;
+    }
+
+    public PodTemplate getTemplate(String template) {
+        for (PodTemplate t : templates) {
+            if (t.getImage().equals(template)) {
+                return t;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Gets {@link PodTemplate} that has the matching {@link Label}.
+     * @param label label to look for in templates
+     * @return the template
+     */
+    public PodTemplate getTemplate(Label label) {
+        for (PodTemplate t : templates) {
+            if (label == null || label.matches(t.getLabelSet())) {
+                return t;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Add a new template to the cloud
+     * @param t docker template
+     */
+    public void addTemplate(PodTemplate t) {
+        this.templates.add(t);
+        // t.parent = this;
+    }
+
+    /**
+     * Remove a
+     * 
+     * @param t docker template
+     */
+    public void removeTemplate(PodTemplate t) {
+        this.templates.remove(t);
+    }
+
+    @Extension
+    public static class DescriptorImpl extends Descriptor<Cloud> {
+        @Override
+        public String getDisplayName() {
+            return "Kubernetes";
+        }
+
+        public FormValidation doTestConnection(@QueryParameter URL serverUrl, @QueryParameter String credentialsId,
+                                               @QueryParameter String serverCertificate,
+                                               @QueryParameter boolean skipTlsVerify,
+                                               @QueryParameter String namespace) throws Exception {
+
+            KubernetesClient client = new KubernetesFactoryAdapter(serverUrl.toExternalForm(),
+                    Util.fixEmpty(serverCertificate), Util.fixEmpty(credentialsId), skipTlsVerify)
+                    .createClient();
+
+            client.pods().inNamespace(namespace).list();
+            return FormValidation.ok("Connection successful");
+        }
+
+        public ListBoxModel doFillCredentialsIdItems(@QueryParameter URL serverUrl) {
+            return new StandardListBoxModel()
+                    .withEmptySelection()
+                    .withMatching(
+                            CredentialsMatchers.anyOf(
+                                    CredentialsMatchers.instanceOf(StandardUsernamePasswordCredentials.class),
+                                    CredentialsMatchers.instanceOf(TokenProducer.class)
+                            ),
+                            CredentialsProvider.lookupCredentials(StandardCredentials.class,
+                                    Jenkins.getInstance(),
+                                    ACL.SYSTEM,
+                                    serverUrl != null ? URIRequirementBuilder.fromUri(serverUrl.toExternalForm()).build()
+                                                      : Collections.EMPTY_LIST
+                            ));
+
+        }
+
+    }
+
+    @Override
+    public String toString() {
+        return String.format("KubernetesCloud name: %n serverUrl: %n", name, serverUrl);
+    }
+
+    private Object readResolve() {
+        if (namespace == null) namespace = "jenkins-slave";
+        return this;
+    }
+
+}
diff --git a/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/KubernetesComputer.java b/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/KubernetesComputer.java
new file mode 100644
index 0000000..f52072c
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/KubernetesComputer.java
@@ -0,0 +1,47 @@
+package org.csanchez.jenkins.plugins.kubernetes;
+
+import hudson.model.Executor;
+import hudson.model.Queue;
+import hudson.slaves.AbstractCloudComputer;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * @author Carlos Sanchez carlos@apache.org
+ */
+public class KubernetesComputer extends AbstractCloudComputer<KubernetesSlave> {
+    private static final Logger LOGGER = Logger.getLogger(KubernetesComputer.class.getName());
+
+    public KubernetesComputer(KubernetesSlave slave) {
+        super(slave);
+    }
+
+    @Override
+    public void taskAccepted(Executor executor, Queue.Task task) {
+        super.taskAccepted(executor, task);
+        LOGGER.fine(" Computer " + this + " taskAccepted");
+    }
+
+    @Override
+    public void taskCompleted(Executor executor, Queue.Task task, long durationMS) {
+        Queue.Executable executable = executor.getCurrentExecutable();
+
+        LOGGER.log(Level.FINE, " Computer " + this + " taskCompleted");
+
+        // May take the slave offline and remove it, in which case getNode()
+        // above would return null and we'd not find our DockerSlave anymore.
+        super.taskCompleted(executor, task, durationMS);
+    }
+
+    @Override
+    public void taskCompletedWithProblems(Executor executor, Queue.Task task, long durationMS, Throwable problems) {
+        super.taskCompletedWithProblems(executor, task, durationMS, problems);
+        LOGGER.log(Level.FINE, " Computer " + this + " taskCompletedWithProblems");
+    }
+
+    @Override
+    public String toString() {
+        return String.format("KubernetesComputer name: %n slave: %n", getName(), getNode());
+    }
+}
\ No newline at end of file
diff --git a/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/KubernetesFactoryAdapter.java b/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/KubernetesFactoryAdapter.java
new file mode 100644
index 0000000..ce663a5
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/KubernetesFactoryAdapter.java
@@ -0,0 +1,73 @@
+package org.csanchez.jenkins.plugins.kubernetes;
+
+import com.cloudbees.plugins.credentials.CredentialsMatchers;
+import com.cloudbees.plugins.credentials.CredentialsProvider;
+import com.cloudbees.plugins.credentials.common.StandardCredentials;
+import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials;
+import com.cloudbees.plugins.credentials.domains.DomainRequirement;
+import hudson.security.ACL;
+import hudson.util.Secret;
+import io.fabric8.kubernetes.client.ConfigBuilder;
+import io.fabric8.kubernetes.client.DefaultKubernetesClient;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import jenkins.model.Jenkins;
+
+import javax.annotation.CheckForNull;
+import java.io.IOException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.util.Collections;
+
+/**
+ * @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
+ */
+public class KubernetesFactoryAdapter {
+
+    private final String serviceAddress;
+    @CheckForNull
+    private final String caCertData;
+    @CheckForNull
+    private final StandardCredentials credentials;
+
+    private final boolean skipTlsVerify;
+
+    public KubernetesFactoryAdapter(String serviceAddress, @CheckForNull String caCertData,
+                                    @CheckForNull String credentials, boolean skipTlsVerify) {
+        this.serviceAddress = serviceAddress;
+        this.caCertData = caCertData;
+        this.credentials = credentials != null ? getCredentials(credentials) : null;
+        this.skipTlsVerify = skipTlsVerify;
+    }
+
+    private StandardCredentials getCredentials(String credentials) {
+        return CredentialsMatchers.firstOrNull(
+                CredentialsProvider.lookupCredentials(StandardCredentials.class,
+                        Jenkins.getInstance(), ACL.SYSTEM, Collections.<DomainRequirement>emptyList()),
+                CredentialsMatchers.withId(credentials)
+        );
+    }
+
+    public KubernetesClient createClient() throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException, IOException {
+        ConfigBuilder builder = new ConfigBuilder().withMasterUrl(serviceAddress);
+        if (credentials != null) {
+            if (credentials instanceof TokenProducer) {
+                final String token = ((TokenProducer)credentials).getToken(serviceAddress, caCertData, skipTlsVerify);
+                builder.withOauthToken(token);
+            }
+            else if (credentials instanceof UsernamePasswordCredentials) {
+                UsernamePasswordCredentials usernamePassword = (UsernamePasswordCredentials) credentials;
+                builder.withUsername(usernamePassword.getUsername()).withPassword(Secret.toString(usernamePassword.getPassword()));
+            }
+        }
+
+        if (skipTlsVerify) {
+            builder.withTrustCerts(true);
+        }
+
+        if (caCertData != null) {
+            builder.withCaCertData(caCertData);
+        }
+        return new DefaultKubernetesClient(builder.build());
+    }
+}
diff --git a/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/KubernetesSlave.java b/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/KubernetesSlave.java
new file mode 100644
index 0000000..a345564
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/KubernetesSlave.java
@@ -0,0 +1,103 @@
+package org.csanchez.jenkins.plugins.kubernetes;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.UUID;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.jenkinsci.plugins.durabletask.executors.OnceRetentionStrategy;
+import org.jvnet.localizer.Localizable;
+import org.jvnet.localizer.ResourceBundleHolder;
+import org.kohsuke.stapler.DataBoundConstructor;
+
+import hudson.Extension;
+import hudson.model.Descriptor;
+import hudson.model.Label;
+import hudson.model.Node;
+import hudson.model.TaskListener;
+import hudson.slaves.AbstractCloudSlave;
+import hudson.slaves.JNLPLauncher;
+import hudson.slaves.NodeProperty;
+import hudson.slaves.OfflineCause;
+import hudson.slaves.RetentionStrategy;
+
+/**
+ * @author Carlos Sanchez carlos@apache.org
+ */
+public class KubernetesSlave extends AbstractCloudSlave {
+
+    private static final Logger LOGGER = Logger.getLogger(KubernetesSlave.class.getName());
+
+    private static final long serialVersionUID = -8642936855413034232L;
+
+    /**
+     * The resource bundle reference
+     */
+    private final static ResourceBundleHolder HOLDER = ResourceBundleHolder.get(Messages.class);
+
+    // private final Pod pod;
+
+    private final KubernetesCloud cloud;
+
+    @DataBoundConstructor
+    public KubernetesSlave(PodTemplate template, String nodeDescription, KubernetesCloud cloud, Label label)
+            throws Descriptor.FormException, IOException {
+        super(Long.toHexString(System.nanoTime()),
+                nodeDescription,
+                template.getRemoteFs(),
+                1,
+                Node.Mode.NORMAL,
+                label == null ? null : label.toString(),
+                new JNLPLauncher(),
+                new OnceRetentionStrategy(cloud.getRetentionTimeout()),
+                Collections.<NodeProperty<Node>> emptyList());
+
+        // this.pod = pod;
+        this.cloud = cloud;
+    }
+
+    @Override
+    public KubernetesComputer createComputer() {
+        return new KubernetesComputer(this);
+    }
+
+    @Override
+    protected void _terminate(TaskListener listener) throws IOException, InterruptedException {
+        LOGGER.log(Level.INFO, "Terminating Kubernetes instance for slave {0}", name);
+
+        if (toComputer() == null) {
+            LOGGER.log(Level.SEVERE, "Computer for slave is null: {0}", name);
+            return;
+        }
+
+        try {
+            cloud.connect().pods().inNamespace(cloud.getNamespace()).withName(name).delete();
+            LOGGER.log(Level.INFO, "Terminated Kubernetes instance for slave {0}", name);
+            toComputer().disconnect(OfflineCause.create(new Localizable(HOLDER, "offline")));
+            LOGGER.log(Level.INFO, "Disconnected computer {0}", name);
+        } catch (Exception e) {
+            LOGGER.log(Level.SEVERE, "Failure to terminate instance for slave " + name, e);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return String.format("KubernetesSlave name: %n", name);
+    }
+
+    @Extension
+    public static final class DescriptorImpl extends SlaveDescriptor {
+
+        @Override
+        public String getDisplayName() {
+            return "Kubernetes Slave";
+        };
+
+        @Override
+        public boolean isInstantiable() {
+            return false;
+        }
+
+    }
+}
diff --git a/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/OpenShiftBearerTokenCredentialImpl.java b/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/OpenShiftBearerTokenCredentialImpl.java
new file mode 100644
index 0000000..2775a8a
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/OpenShiftBearerTokenCredentialImpl.java
@@ -0,0 +1,173 @@
+package org.csanchez.jenkins.plugins.kubernetes;
+
+import com.cloudbees.plugins.credentials.CredentialsScope;
+import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;
+import hudson.Extension;
+import hudson.util.Secret;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.binary.Base64InputStream;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+import org.apache.http.ProtocolException;
+import org.apache.http.client.RedirectStrategy;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.utils.URLEncodedUtils;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.ssl.SSLContextBuilder;
+import org.apache.http.ssl.TrustStrategy;
+import org.kohsuke.stapler.DataBoundConstructor;
+
+import javax.net.ssl.HostnameVerifier;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.Charset;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
+ */
+public class OpenShiftBearerTokenCredentialImpl extends UsernamePasswordCredentialsImpl implements TokenProducer {
+
+    private transient AtomicReference<Token> token = new AtomicReference<Token>();
+
+    @DataBoundConstructor
+    public OpenShiftBearerTokenCredentialImpl(CredentialsScope scope, String id, String description, String username, String password) {
+        super(scope, id, description, username, password);
+    }
+
+    private Object readResolve() {
+        token = new AtomicReference<Token>();
+        return this;
+    }
+
+
+    @Override
+    public String getToken(String serviceAddress, String caCertData, boolean skipTlsVerify) throws IOException {
+        Token t = this.token.get();
+        if (t == null || System.currentTimeMillis() > t.expire) {
+            t = refreshToken(serviceAddress, caCertData, skipTlsVerify);
+        }
+
+        return t.value;
+    }
+
+    private synchronized Token refreshToken(String serviceAddress, String caCertData, boolean skipTlsVerify) throws IOException {
+
+        URI uri = null;
+        try {
+            uri = new URI(serviceAddress);
+        } catch (URISyntaxException e) {
+            throw new IOException("Invalid server URL "+serviceAddress, e);
+        }
+
+        final HttpClientBuilder builder = HttpClients.custom()
+                .setRedirectStrategy(NO_REDIRECT);
+
+        if (skipTlsVerify || caCertData != null) {
+            final SSLContextBuilder sslBuilder = new SSLContextBuilder();
+            HostnameVerifier hostnameVerifier = SSLConnectionSocketFactory.getDefaultHostnameVerifier();
+            try {
+                if (skipTlsVerify) {
+                    sslBuilder.loadTrustMaterial(null, ALWAYS);
+                    hostnameVerifier = NoopHostnameVerifier.INSTANCE;
+                }
+                else if (caCertData != null) {
+                    KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
+                    ks.load(null);
+                    CertificateFactory f = CertificateFactory.getInstance("X509");
+                    X509Certificate cert = (X509Certificate) f.generateCertificate(new Base64InputStream(new ByteArrayInputStream(caCertData.getBytes())));
+                    ks.setCertificateEntry(uri.getHost(), cert);
+                    sslBuilder.loadTrustMaterial(ks, null);
+                }
+
+                builder.setSSLSocketFactory(new SSLConnectionSocketFactory(sslBuilder.build(), hostnameVerifier));
+            } catch (NoSuchAlgorithmException e) {
+                e.printStackTrace();
+            } catch (KeyManagementException e) {
+                e.printStackTrace();
+            } catch (KeyStoreException e) {
+                e.printStackTrace();
+            } catch (CertificateException e) {
+                e.printStackTrace();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+
+
+        HttpGet authorize = new HttpGet(serviceAddress + "/oauth/authorize?client_id=openshift-challenging-client&response_type=token");
+        authorize.setHeader("Authorization", "Basic "+ Base64.encodeBase64String(
+                (getUsername()+':'+Secret.toString(getPassword()))
+                .getBytes()));
+        final CloseableHttpResponse response = builder.build().execute(authorize);
+
+        if (response.getStatusLine().getStatusCode() != 302) {
+            throw new IOException("Failed to get an OAuth access token " + response.getStatusLine().getStatusCode());
+        }
+
+        String location = response.getFirstHeader("Location").getValue();
+        String parameters = location.substring(location.indexOf('#')+1);
+        List<NameValuePair> pairs = URLEncodedUtils.parse(parameters, Charset.forName("UTF-8"));
+        Token t = new Token();
+        for (NameValuePair pair : pairs) {
+            if (pair.getName().equals("access_token")) {
+                t.value = pair.getValue();
+            }
+            else if (pair.getName().equals("expires_in")) {
+                t.expire = System.currentTimeMillis() + Long.parseLong(pair.getValue())*1000 - 100;
+            }
+        }
+        return t;
+    }
+
+    @Extension
+    public static class DescriptorImpl extends BaseStandardCredentialsDescriptor {
+
+        @Override
+        public String getDisplayName() {
+            return "OpenShift OAuth Access token";
+        }
+    }
+
+    private static class Token {
+        String value;
+        long expire;
+    }
+
+    private static TrustStrategy ALWAYS = new TrustStrategy() {
+
+        @Override
+        public boolean isTrusted(final X509Certificate[] chain, final String authType) throws CertificateException {
+            return true;
+        }
+    };
+
+    private static RedirectStrategy NO_REDIRECT = new RedirectStrategy() {
+        @Override
+        public boolean isRedirected(HttpRequest request, HttpResponse response, HttpContext context) throws ProtocolException {
+            return false;
+        }
+
+        @Override
+        public HttpUriRequest getRedirect(HttpRequest request, HttpResponse response, HttpContext context) throws ProtocolException {
+            return null;
+        }
+    };
+}
diff --git a/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplate.java b/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplate.java
new file mode 100644
index 0000000..c104955
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplate.java
@@ -0,0 +1,154 @@
+package org.csanchez.jenkins.plugins.kubernetes;
+
+import hudson.Extension;
+import hudson.model.AbstractDescribableImpl;
+import hudson.model.Descriptor;
+import hudson.model.Label;
+import hudson.model.labels.LabelAtom;
+
+import org.apache.commons.lang.StringUtils;
+import org.kohsuke.stapler.DataBoundConstructor;
+import org.kohsuke.stapler.DataBoundSetter;
+
+import com.google.common.base.Preconditions;
+import org.csanchez.jenkins.plugins.kubernetes.PodVolumes.PodVolume;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
+ */
+public class PodTemplate extends AbstractDescribableImpl<PodTemplate> {
+
+    private String name;
+
+    private final String image;
+
+    private boolean privileged;
+
+    private String command;
+
+    private String args;
+
+    private String remoteFs;
+
+    private int instanceCap;
+
+    private String label;
+
+    private final List<PodVolume> volumes;
+
+    @DataBoundConstructor
+    public PodTemplate(String image, List<? extends PodVolume> volumes) {
+        Preconditions.checkArgument(!StringUtils.isBlank(image));
+        this.image = image;
+        this.volumes = (volumes == null) ? new ArrayList<PodVolume>() :
+                new ArrayList<PodVolume>(volumes);
+    }
+
+    @DataBoundSetter
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getImage() {
+        return image;
+    }
+
+    @DataBoundSetter
+    public void setCommand(String command) {
+        this.command = command;
+    }
+
+    public String getCommand() {
+        return command;
+    }
+
+    @DataBoundSetter
+    public void setArgs(String args) {
+        this.args = args;
+    }
+
+    public String getArgs() {
+        return args;
+    }
+
+    public String getDisplayName() {
+        return "Kubernetes Pod Template";
+    }
+
+    @DataBoundSetter
+    public void setRemoteFs(String remoteFs) {
+        this.remoteFs = StringUtils.isBlank(remoteFs) ? "/home/jenkins" : remoteFs;
+    }
+
+    public String getRemoteFs() {
+        return remoteFs;
+    }
+
+    public void setInstanceCap(int instanceCap) {
+        this.instanceCap = instanceCap;
+    }
+
+    public int getInstanceCap() {
+        return instanceCap;
+    }
+
+    @DataBoundSetter
+    public void setInstanceCapStr(String instanceCapStr) {
+        if ("".equals(instanceCapStr)) {
+            setInstanceCap(Integer.MAX_VALUE);
+        } else {
+            setInstanceCap(Integer.parseInt(instanceCapStr));
+        }
+    }
+
+    public String getInstanceCapStr() {
+        if (getInstanceCap() == Integer.MAX_VALUE) {
+            return "";
+        } else {
+            return String.valueOf(instanceCap);
+        }
+    }
+
+    public Set<LabelAtom> getLabelSet() {
+        return Label.parse(label);
+    }
+
+    @DataBoundSetter
+    public void setLabel(String label) {
+        this.label = label;
+    }
+
+    public String getLabel() {
+        return label;
+    }
+
+    @DataBoundSetter
+    public void setPrivileged(boolean privileged) {
+        this.privileged = privileged;
+    }
+
+    public boolean isPrivileged() {
+        return privileged;
+    }
+
+    @Extension
+    public static class DescriptorImpl extends Descriptor<PodTemplate> {
+
+        @Override
+        public String getDisplayName() {
+            return "Kubernetes Pod Template";
+        }
+    }
+
+    public List<PodVolume> getVolumes() {
+        return volumes;
+    }
+}
diff --git a/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodVolumes.java b/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodVolumes.java
new file mode 100644
index 0000000..4b296bb
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodVolumes.java
@@ -0,0 +1,55 @@
+package org.csanchez.jenkins.plugins.kubernetes;
+
+import hudson.Extension;
+import hudson.model.AbstractDescribableImpl;
+import hudson.model.Descriptor;
+import io.fabric8.kubernetes.api.model.Volume;
+import io.fabric8.kubernetes.api.model.VolumeBuilder;
+import org.kohsuke.stapler.DataBoundConstructor;
+
+public class PodVolumes {
+    /**
+     * Base class for all Kubernetes volume types
+     */
+    public static abstract class PodVolume extends AbstractDescribableImpl<PodVolume> {
+        // Where to mount this volume in the pod.
+        public abstract String getMountPath();
+
+        // Builds a Volume model with the given name.
+        public abstract Volume buildVolume(String volumeName);
+    }
+
+    public static class HostPathVolume extends PodVolume {
+        private String mountPath;
+        private String hostPath;
+
+        @DataBoundConstructor
+        public HostPathVolume(String hostPath, String mountPath) {
+            this.hostPath = hostPath;
+            this.mountPath = mountPath;
+        }
+
+        public Volume buildVolume(String volumeName) {
+            return new VolumeBuilder()
+                    .withName(volumeName)
+                    .withNewHostPath(getHostPath())
+                    .build();
+        }
+
+        public String getMountPath() {
+            return mountPath;
+        }
+
+        public String getHostPath() {
+            return hostPath;
+        }
+
+        @Extension
+        public static class DescriptorImpl extends Descriptor<PodVolume> {
+            @Override
+            public String getDisplayName() {
+                return "Host Path Volume";
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/ServiceAccountCredential.java b/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/ServiceAccountCredential.java
new file mode 100644
index 0000000..523194f
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/ServiceAccountCredential.java
@@ -0,0 +1,49 @@
+package org.csanchez.jenkins.plugins.kubernetes;
+
+import com.cloudbees.plugins.credentials.CredentialsScope;
+import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials;
+import hudson.Extension;
+import org.apache.commons.io.FileUtils;
+import org.kohsuke.stapler.DataBoundConstructor;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Read the OAuth bearer token from service account file provisionned by kubernetes
+ * <a href="http://kubernetes.io/v1.0/docs/admin/service-accounts-admin.html">Service Account Admission Controller</a>
+ * when Jenkins itself is deployed inside a Pod.
+ *
+ * @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
+ */
+public class ServiceAccountCredential extends BaseStandardCredentials implements TokenProducer {
+
+    @DataBoundConstructor
+    public ServiceAccountCredential(CredentialsScope scope, String id, String description) {
+        super(scope, id, description);
+    }
+
+    @Override
+    public String getToken(String serviceAddress, String caCertData, boolean skipTlsVerify) {
+        try {
+            return FileUtils.readFileToString(new File("/run/secrets/kubernetes.io/serviceaccount/token"));
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    @Extension(optional = true)
+    public static class DescriptorImpl extends BaseStandardCredentialsDescriptor {
+
+        public DescriptorImpl() {
+            if (!new File("/run/secrets/kubernetes.io/serviceaccount/token").exists()) {
+                throw new RuntimeException("Jenkins isn't running inside Kubernetes with Admission Controller.");
+            }
+        }
+
+        @Override
+        public String getDisplayName() {
+            return "Kubernetes Service Account";
+        }
+    }
+}
diff --git a/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/TokenProducer.java b/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/TokenProducer.java
new file mode 100644
index 0000000..2851097
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/java/org/csanchez/jenkins/plugins/kubernetes/TokenProducer.java
@@ -0,0 +1,11 @@
+package org.csanchez.jenkins.plugins.kubernetes;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+
+/**
+ * @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
+ */
+public interface TokenProducer {
+    String getToken(String serviceAddress, String caCertData, boolean skipTlsVerify) throws IOException;
+}
diff --git a/java/jenkins_plugins/kubernetes/src/main/kubernetes/jenkins-gke.yml b/java/jenkins_plugins/kubernetes/src/main/kubernetes/jenkins-gke.yml
new file mode 100644
index 0000000..0f78297
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/kubernetes/jenkins-gke.yml
@@ -0,0 +1,35 @@
+---
+  apiVersion: "v1"
+  kind: "ReplicationController"
+  metadata: 
+    name: "jenkins"
+    labels: 
+      name: "jenkins"
+  spec: 
+    replicas: 1
+    template: 
+      metadata: 
+        name: "jenkins"
+        labels: 
+          name: "jenkins"
+      spec: 
+        containers: 
+          - name: "jenkins"
+            image: "csanchez/jenkins-kubernetes:latest"
+            ports: 
+              - containerPort: 8080
+              - containerPort: 50000
+            volumeMounts: 
+              - name: "jenkins-data"
+                mountPath: "/var/jenkins_home"
+            livenessProbe:
+              httpGet:
+                path: /
+                port: 8080
+              initialDelaySeconds: 60
+              timeoutSeconds: 5
+        volumes: 
+          - name: "jenkins-data"
+            gcePersistentDisk:
+              pdName: kubernetes-jenkins
+              fsType: ext4
diff --git a/java/jenkins_plugins/kubernetes/src/main/kubernetes/jenkins-local.yml b/java/jenkins_plugins/kubernetes/src/main/kubernetes/jenkins-local.yml
new file mode 100644
index 0000000..4ecbda4
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/kubernetes/jenkins-local.yml
@@ -0,0 +1,34 @@
+---
+  apiVersion: "v1"
+  kind: "ReplicationController"
+  metadata: 
+    name: "jenkins"
+    labels: 
+      name: "jenkins"
+  spec: 
+    replicas: 1
+    template: 
+      metadata: 
+        name: "jenkins"
+        labels: 
+          name: "jenkins"
+      spec: 
+        containers: 
+          - name: "jenkins"
+            image: "csanchez/jenkins-kubernetes:latest"
+            ports: 
+              - containerPort: 8080
+              - containerPort: 50000
+            volumeMounts: 
+              - name: "jenkins-data"
+                mountPath: "/var/jenkins_home"
+            livenessProbe:
+              httpGet:
+                path: /
+                port: 8080
+              initialDelaySeconds: 60
+              timeoutSeconds: 5
+        volumes: 
+          - name: "jenkins-data"
+            hostPath:
+              path: "/var/jenkins"
diff --git a/java/jenkins_plugins/kubernetes/src/main/kubernetes/service-gke.yml b/java/jenkins_plugins/kubernetes/src/main/kubernetes/service-gke.yml
new file mode 100644
index 0000000..777a6ef
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/kubernetes/service-gke.yml
@@ -0,0 +1,19 @@
+---
+  apiVersion: "v1"
+  kind: "Service"
+  metadata: 
+    name: "jenkins"
+  spec: 
+    type: "LoadBalancer"
+    selector: 
+      name: "jenkins"
+    ports: 
+      - 
+        name: "http"
+        port: 80
+        targetPort: 8080
+        protocol: "TCP"
+      - 
+        name: "slave"
+        port: 50000
+        protocol: "TCP"
diff --git a/java/jenkins_plugins/kubernetes/src/main/kubernetes/service-local.yml b/java/jenkins_plugins/kubernetes/src/main/kubernetes/service-local.yml
new file mode 100644
index 0000000..4f850c0
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/kubernetes/service-local.yml
@@ -0,0 +1,18 @@
+---
+  apiVersion: "v1"
+  kind: "Service"
+  metadata: 
+    name: "jenkins"
+  spec: 
+    type: "NodePort"
+    selector: 
+      name: "jenkins"
+    ports: 
+      - 
+        name: "http"
+        port: 8080
+        protocol: "TCP"
+      - 
+        name: "slave"
+        port: 50000
+        protocol: "TCP"
diff --git a/java/jenkins_plugins/kubernetes/src/main/resources/index.jelly b/java/jenkins_plugins/kubernetes/src/main/resources/index.jelly
new file mode 100644
index 0000000..d801357
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/resources/index.jelly
@@ -0,0 +1,4 @@
+<div>
+  This plugin integrates Jenkins with
+  <a href="https://github.com/GoogleCloudPlatform/kubernetes/">Kubernetes</a>
+</div>
diff --git a/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubectlBuildWrapper/config.jelly b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubectlBuildWrapper/config.jelly
new file mode 100644
index 0000000..a7248e4
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubectlBuildWrapper/config.jelly
@@ -0,0 +1,12 @@
+<?jelly escape-by-default='true'?>
+<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:c="/lib/credentials">
+
+  <f:entry field="serverUrl" title="${%Kubernetes server endpoint}">
+    <f:textbox/>
+  </f:entry>
+
+  <f:entry field="credentialsId" title="${%Credentials}">
+    <c:select/>
+  </f:entry>
+
+</j:jelly>
diff --git a/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/config.jelly b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/config.jelly
new file mode 100644
index 0000000..ff826e2
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/config.jelly
@@ -0,0 +1,61 @@
+<?jelly escape-by-default='true'?>
+<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:c="/lib/credentials">
+
+    <f:entry title="${%Name}" field="name">
+      <f:textbox />
+    </f:entry>
+
+    <f:entry title="${%Kubernetes URL}" field="serverUrl">
+      <f:textbox />
+    </f:entry>
+
+    <f:entry title="${%Kubernetes server certificate key}" field="serverCertificate">
+      <f:textarea/>
+    </f:entry>
+
+    <f:entry title="${%Disable https certificate check}" field="skipTlsVerify">
+      <f:checkbox />
+    </f:entry>
+
+    <f:entry title="${%Kubernetes Namespace}" field="namespace">
+      <f:textbox default="default" />
+    </f:entry>
+
+    <f:entry title="${%Credentials}" field="credentialsId">
+      <c:select/>
+    </f:entry>
+
+    <f:validateButton title="${%Test Connection}" progress="${%Testing...}" method="testConnection" with="serverUrl,credentialsId,serverCertificate,skipTlsVerify,namespace" />
+
+    <f:entry title="${%Jenkins URL}" field="jenkinsUrl">
+      <f:textbox />
+    </f:entry>
+
+    <f:entry title="${%Jenkins tunnel}" field="jenkinsTunnel">
+      <f:textbox />
+    </f:entry>
+
+    <f:entry title="${%Connection Timeout (seconds)}" field="connectTimeout">
+        <f:textbox default="5"/>
+    </f:entry>
+
+    <f:entry title="${%Read Timeout (seconds)}" field="readTimeout">
+        <f:textbox default="15"/>
+    </f:entry>
+
+    <f:entry title="${%Container Cap}" field="containerCapStr">
+        <f:textbox default="10"/>
+    </f:entry>
+
+    <f:advanced>
+      <f:entry title="${%Container Cleanup Timeout (minutes)}" field="retentionTimeout">
+        <f:textbox default="5"/>
+      </f:entry>
+    </f:advanced>
+
+    <f:entry title="${%Images}" description="${%List of Images to be launched as slaves}">
+      <f:repeatableHeteroProperty field="templates" hasHeader="true" addCaption="Add Pod Template"
+                                  deleteCaption="Delete Template" />
+    </f:entry>
+
+</j:jelly>
diff --git a/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-connectTimeout.html b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-connectTimeout.html
new file mode 100644
index 0000000..f1ffa68
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-connectTimeout.html
@@ -0,0 +1,3 @@
+<div>
+    (currently unused)
+</div>
\ No newline at end of file
diff --git a/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-containerCapStr.html b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-containerCapStr.html
new file mode 100644
index 0000000..770613a
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-containerCapStr.html
@@ -0,0 +1,3 @@
+<div>
+    The maximum number of concurrently running slave containers that Kubernetes is allowed to run.
+</div>
\ No newline at end of file
diff --git a/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-jenkinsTunnel.html b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-jenkinsTunnel.html
new file mode 100644
index 0000000..3cb5aac
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-jenkinsTunnel.html
@@ -0,0 +1,5 @@
+<div>
+    Connect to the specified host and port, instead of connecting directly to Jenkins.
+    Useful when connection to Hudson needs to be tunneled. Can be also HOST: or :PORT,
+    in which case the missing portion will be auto-configured like the default behavior
+</div>
\ No newline at end of file
diff --git a/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-jenkinsUrl.html b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-jenkinsUrl.html
new file mode 100644
index 0000000..fca15de
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-jenkinsUrl.html
@@ -0,0 +1,3 @@
+<div>
+    The URL of the Jenkins Master server.
+</div>
\ No newline at end of file
diff --git a/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-name.html b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-name.html
new file mode 100644
index 0000000..adbda07
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-name.html
@@ -0,0 +1,5 @@
+<div>
+    Uniquely identifies this Cloud instance among other instances in Jenkins Clouds.
+    This is expected to be short ID-like string that does not contain any character unsafe as variable name or
+    URL path token.
+</div>
diff --git a/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-password.html b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-password.html
new file mode 100644
index 0000000..4ea0611
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-password.html
@@ -0,0 +1,3 @@
+<div>
+    The password of the authorized user on the Kubernetes API server.  Can be blank for unsecured access.
+</div>
\ No newline at end of file
diff --git a/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-readTimeout.html b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-readTimeout.html
new file mode 100644
index 0000000..f1ffa68
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-readTimeout.html
@@ -0,0 +1,3 @@
+<div>
+    (currently unused)
+</div>
\ No newline at end of file
diff --git a/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-retentionTimeout.html b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-retentionTimeout.html
new file mode 100644
index 0000000..9692847
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-retentionTimeout.html
@@ -0,0 +1,7 @@
+Time in minutes after which the Kubernetes cloud plugin will clean up an idle
+worker that has not already terminated. This cleanup is only necessary in
+exceptional conditions; typically workers will terminate upon completion of the
+invoking task.
+
+<p>For tasks that use very large images, this timeout can be increased to avoid
+early termination of the task while the Kubernetes pod is still deploying.</p>
diff --git a/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-serverUrl.html b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-serverUrl.html
new file mode 100644
index 0000000..67ed505
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-serverUrl.html
@@ -0,0 +1,3 @@
+<div>
+    The URL of the Kubernetes API server.
+</div>
\ No newline at end of file
diff --git a/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-skipTlsVerify.html b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-skipTlsVerify.html
new file mode 100644
index 0000000..6ce90fa
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-skipTlsVerify.html
@@ -0,0 +1,5 @@
+With this option enabled, communication with kubernetes API master will rely on https but will fully ignore ssl
+certificate verification. This is usefull for quick setup but does make your installation unsecured, so please consider
+twice.
+<p>
+Alternatively, capture API server certificate and register it as Kubernetes server certificate key.
\ No newline at end of file
diff --git a/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-username.html b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-username.html
new file mode 100644
index 0000000..691179b
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloud/help-username.html
@@ -0,0 +1,3 @@
+<div>
+    The user name of an authorized user on the Kubernetes API server.  Can be blank for unsecured access.
+</div>
\ No newline at end of file
diff --git a/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesSlave/configure-entries.jelly b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesSlave/configure-entries.jelly
new file mode 100644
index 0000000..a61a3b7
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/KubernetesSlave/configure-entries.jelly
@@ -0,0 +1,50 @@
+<!--
+The MIT License
+
+Copyright (c) 2004-2009, Sun Microsystems, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+-->
+
+<!--
+  Config page
+-->
+<?jelly escape-by-default='true'?>
+<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout"
+         xmlns:t="/lib/hudson" xmlns:f="/lib/form">
+  <f:entry title="${%Description}" help="/help/system-config/master-slave/description.html">
+    <f:textbox field="nodeDescription" />
+  </f:entry>
+
+  <f:entry title="${%# of executors}" field="numExecutors">
+    <f:textbox />
+  </f:entry>
+
+  <f:entry title="${%Remote FS root}" field="remoteFS">
+    <f:textbox />
+  </f:entry>
+
+  <f:entry title="${%Labels}" field="labelString">
+    <f:textbox />
+  </f:entry>
+
+  <f:slave-mode name="mode" node="${it}" />
+
+  <f:descriptorList title="${%Node Properties}" descriptors="${h.getNodePropertyDescriptors(descriptor.clazz)}" field="nodeProperties" />
+</j:jelly>
\ No newline at end of file
diff --git a/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/Messages.properties b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/Messages.properties
new file mode 100644
index 0000000..3675a2d
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/Messages.properties
@@ -0,0 +1,8 @@
+
+
+PluginDescription=\
+  Plugin for launching Slaves on Kubernetes
+DisplayName=\
+  Kubernetes
+
+offline=Kubernetes slave is going offline
diff --git a/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/OpenShiftBearerTokenCredentialImpl/help.html b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/OpenShiftBearerTokenCredentialImpl/help.html
new file mode 100644
index 0000000..fc8b3ba
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/OpenShiftBearerTokenCredentialImpl/help.html
@@ -0,0 +1,4 @@
+OpenShift do use a dedicated authorization layer on top of Kubernetes and does not allow to access Kubernetes API using plain username/password credentials.
+This credential type do offer to generate OAuth access token based on your credentials as an alternative.
+<p>
+Read more on <a href="https://docs.openshift.com/enterprise/3.0/architecture/additional_concepts/authentication.html#oauth">authentication documentation</a>.
\ No newline at end of file
diff --git a/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/config.jelly b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/config.jelly
new file mode 100644
index 0000000..c30c850
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/config.jelly
@@ -0,0 +1,71 @@
+<!--
+The MIT License
+
+Copyright (c) 2004-2009, Sun Microsystems, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+-->
+
+<!--
+  Config page
+-->
+<?jelly escape-by-default='true'?>
+<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout"
+         xmlns:t="/lib/hudson" xmlns:f="/lib/form">
+
+  <f:entry field="name" title="${%Name}">
+    <f:textbox/>
+  </f:entry>
+
+  <f:entry field="label" title="${%Labels}">
+    <f:textbox/>
+  </f:entry>
+
+  <f:entry field="image" title="${%Docker image}">
+    <f:textbox/>
+  </f:entry>
+
+  <f:entry field="remoteFs" title="${%Jenkins slave root directory}">
+    <f:textbox default="/home/jenkins"/>
+  </f:entry>
+
+  <f:entry field="command" title="${%Command to run slave agent}">
+    <f:textbox/>
+  </f:entry>
+
+  <f:entry field="args" title="${%Arguments to pass to the command}">
+    <f:textbox/>
+  </f:entry>
+
+  <f:entry field="instanceCapStr" title="${%Max number of instances}">
+    <f:textbox/>
+  </f:entry>
+
+  <f:entry title="${%Volumes}" description="${%List of volumes to mount in slave pod}">
+    <f:repeatableHeteroProperty field="volumes" hasHeader="true" addCaption="Add Volume"
+                                deleteCaption="Delete Volume" />
+  </f:entry>
+
+  <f:advanced>
+    <f:entry field="privileged" title="${%Run in privileged mode}">
+      <f:checkbox/>
+    </f:entry>
+  </f:advanced>
+
+</j:jelly>
\ No newline at end of file
diff --git a/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/help-args.html b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/help-args.html
new file mode 100644
index 0000000..a30c524
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/help-args.html
@@ -0,0 +1 @@
+Arguments to pass to the command. The slave secret and name are always added to the arguments. 
\ No newline at end of file
diff --git a/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/help-command.html b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/help-command.html
new file mode 100644
index 0000000..095e37d
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/help-command.html
@@ -0,0 +1 @@
+Override the image entrypoint with a different one
\ No newline at end of file
diff --git a/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/help-image.html b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/help-image.html
new file mode 100644
index 0000000..7eeb676
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/help-image.html
@@ -0,0 +1,5 @@
+Docker image ID for a jenkins JNLP slave.
+This image is responsible to run a jenkins jnlp bootstrap agent and connect to Jenkins master.
+Secret key and slave name as well as jenkins callback URL are passed as argument as expected
+by <code>hudson.remoting.jnlp.Main</code>.
+
diff --git a/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/help-remoteFs.html b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/help-remoteFs.html
new file mode 100644
index 0000000..426ddd4
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/help-remoteFs.html
@@ -0,0 +1,2 @@
+Path to the root of the workspace from the view point of this node, such as "/var/jenkins_home", this need not
+be absolute provided that the launcher establishes a consistent working directory, such as "./.jenkins-slave".
diff --git a/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodVolumes/HostPathVolume/config.jelly b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodVolumes/HostPathVolume/config.jelly
new file mode 100644
index 0000000..2b039a4
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodVolumes/HostPathVolume/config.jelly
@@ -0,0 +1,41 @@
+<!--
+The MIT License
+
+Copyright (c) 2004-2009, Sun Microsystems, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+-->
+
+<!--
+  Host Volume Path
+-->
+
+<?jelly escape-by-default='true'?>
+<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout"
+         xmlns:t="/lib/hudson" xmlns:f="/lib/form">
+
+  <f:entry title="${%Host path}" field="hostPath">
+    <f:textbox />
+  </f:entry>
+
+  <f:entry title="${%Mount path}" field="mountPath">
+    <f:textbox />
+  </f:entry>
+
+</j:jelly>
\ No newline at end of file
diff --git a/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodVolumes/HostPathVolume/help-hostPath.html b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodVolumes/HostPathVolume/help-hostPath.html
new file mode 100644
index 0000000..f8271d9
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodVolumes/HostPathVolume/help-hostPath.html
@@ -0,0 +1 @@
+File or directory on the host node's filesystem to mount into the pod.
\ No newline at end of file
diff --git a/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodVolumes/HostPathVolume/help-mountPath.html b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodVolumes/HostPathVolume/help-mountPath.html
new file mode 100644
index 0000000..9952c26
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodVolumes/HostPathVolume/help-mountPath.html
@@ -0,0 +1 @@
+Path to mount this voume inside the pod.
diff --git a/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/ServiceAccountCredential/credentials.jelly b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/ServiceAccountCredential/credentials.jelly
new file mode 100644
index 0000000..c89ef79
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/ServiceAccountCredential/credentials.jelly
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form" xmlns:st="jelly:stapler">
+  <st:include page="id-and-description" class="${descriptor.clazz}"/>
+</j:jelly>
diff --git a/java/jenkins_plugins/kubernetes/src/main/webapp/images/24x24/kubernetes.png b/java/jenkins_plugins/kubernetes/src/main/webapp/images/24x24/kubernetes.png
new file mode 100644
index 0000000..912dfe0
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/webapp/images/24x24/kubernetes.png
Binary files differ
diff --git a/java/jenkins_plugins/kubernetes/src/main/webapp/images/32x32/kubernetes.png b/java/jenkins_plugins/kubernetes/src/main/webapp/images/32x32/kubernetes.png
new file mode 100644
index 0000000..eda97ba
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/webapp/images/32x32/kubernetes.png
Binary files differ
diff --git a/java/jenkins_plugins/kubernetes/src/main/webapp/images/48x48/kubernetes.png b/java/jenkins_plugins/kubernetes/src/main/webapp/images/48x48/kubernetes.png
new file mode 100644
index 0000000..274d0ce
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/main/webapp/images/48x48/kubernetes.png
Binary files differ
diff --git a/java/jenkins_plugins/kubernetes/src/test/java/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloudTest.java b/java/jenkins_plugins/kubernetes/src/test/java/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloudTest.java
new file mode 100644
index 0000000..8f6ca0e
--- /dev/null
+++ b/java/jenkins_plugins/kubernetes/src/test/java/org/csanchez/jenkins/plugins/kubernetes/KubernetesCloudTest.java
@@ -0,0 +1,24 @@
+package org.csanchez.jenkins.plugins.kubernetes;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+
+public class KubernetesCloudTest {
+
+    private KubernetesCloud cloud = new KubernetesCloud("test", null, "http://localhost:8080", "default", null, "", 0,
+            0, /*retentionTimeoutMinutes=*/ 5);
+
+    @Test
+    public void testParseDockerCommand() {
+        assertNull(cloud.parseDockerCommand(""));
+        assertNull(cloud.parseDockerCommand(null));
+        assertEquals(ImmutableList.of("bash"), cloud.parseDockerCommand("bash"));
+        assertEquals(ImmutableList.of("bash", "-c", "x y"), cloud.parseDockerCommand("bash -c \"x y\""));
+        assertEquals(ImmutableList.of("a", "b", "c", "d"), cloud.parseDockerCommand("a b c d"));
+    }
+
+}