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"));
+ }
+
+}