From 7177e66e8c4161774b39d74aeb08346c466a0921 Mon Sep 17 00:00:00 2001 From: Oleg Nenashev Date: Wed, 27 May 2015 21:11:14 +0300 Subject: [PATCH] Initial commit --- .gitignore | 7 + LICENSE.txt | 20 ++ README.md | 22 ++ demo/.dockerignore | 1 + demo/Dockerfile_release | 59 +++ demo/Dockerfile_snapshot | 60 ++++ demo/JENKINS_HOME/config.xml | 34 ++ demo/JENKINS_HOME/credentials.xml | 19 + .../jobs/docker-workflow/config.xml | 72 ++++ demo/Makefile | 73 ++++ demo/README.md | 31 ++ demo/gen-security-data.sh | 53 +++ demo/get-snapshots.sh | 59 +++ demo/plugins.txt | 3 + demo/run-demo.sh | 55 +++ demo/workflow-reg-proxy.conf | 37 ++ demo/workflow-version.txt | 1 + demo/wrapdocker | 137 +++++++ pom.xml | 106 ++++++ .../AbstractEndpointStepExecution.java | 101 ++++++ .../plugins/docker/workflow/DockerDSL.java | 67 ++++ .../docker/workflow/FromFingerprintStep.java | 140 ++++++++ .../plugins/docker/workflow/ImageAction.java | 90 +++++ .../docker/workflow/ImageNameTokens.java | 65 ++++ .../docker/workflow/RegistryEndpointStep.java | 90 +++++ .../docker/workflow/RunFingerprintStep.java | 105 ++++++ .../docker/workflow/ServerEndpointStep.java | 90 +++++ .../docker/workflow/WithContainerStep.java | 283 +++++++++++++++ .../docker/workflow/client/DockerClient.java | 271 ++++++++++++++ .../docker/workflow/client/LaunchResult.java | 70 ++++ src/main/resources/index.jelly | 5 + .../plugins/docker/workflow/Docker.groovy | 179 ++++++++++ .../docker/workflow/DockerDSL/help.jelly | 120 +++++++ .../workflow/FromFingerprintStep/config.jelly | 34 ++ .../FromFingerprintStep/help-dockerfile.html | 3 + .../FromFingerprintStep/help-image.html | 3 + .../workflow/FromFingerprintStep/help.html | 4 + .../RegistryEndpointStep/config.jelly | 28 ++ .../workflow/RegistryEndpointStep/help.html | 4 + .../workflow/RunFingerprintStep/config.jelly | 31 ++ .../workflow/RunFingerprintStep/help.html | 4 + .../workflow/ServerEndpointStep/config.jelly | 28 ++ .../workflow/ServerEndpointStep/help.html | 4 + .../workflow/WithContainerStep/config.jelly | 34 ++ .../workflow/WithContainerStep/help-args.html | 3 + .../workflow/WithContainerStep/help.html | 7 + .../docker/workflow/DockerDSLTest.java | 338 ++++++++++++++++++ .../docker/workflow/DockerTestUtil.java | 53 +++ .../docker/workflow/ImageNameTokensTest.java | 67 ++++ .../workflow/RegistryEndpointStepTest.java | 72 ++++ .../workflow/ServerEndpointStepTest.java | 101 ++++++ .../workflow/WithContainerStepTest.java | 124 +++++++ .../workflow/client/DockerClientTest.java | 108 ++++++ 53 files changed, 3575 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 demo/.dockerignore create mode 100644 demo/Dockerfile_release create mode 100644 demo/Dockerfile_snapshot create mode 100644 demo/JENKINS_HOME/config.xml create mode 100644 demo/JENKINS_HOME/credentials.xml create mode 100644 demo/JENKINS_HOME/jobs/docker-workflow/config.xml create mode 100644 demo/Makefile create mode 100644 demo/README.md create mode 100755 demo/gen-security-data.sh create mode 100755 demo/get-snapshots.sh create mode 100644 demo/plugins.txt create mode 100755 demo/run-demo.sh create mode 100644 demo/workflow-reg-proxy.conf create mode 100644 demo/workflow-version.txt create mode 100755 demo/wrapdocker create mode 100644 pom.xml create mode 100644 src/main/java/org/jenkinsci/plugins/docker/workflow/AbstractEndpointStepExecution.java create mode 100644 src/main/java/org/jenkinsci/plugins/docker/workflow/DockerDSL.java create mode 100644 src/main/java/org/jenkinsci/plugins/docker/workflow/FromFingerprintStep.java create mode 100644 src/main/java/org/jenkinsci/plugins/docker/workflow/ImageAction.java create mode 100644 src/main/java/org/jenkinsci/plugins/docker/workflow/ImageNameTokens.java create mode 100644 src/main/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStep.java create mode 100644 src/main/java/org/jenkinsci/plugins/docker/workflow/RunFingerprintStep.java create mode 100644 src/main/java/org/jenkinsci/plugins/docker/workflow/ServerEndpointStep.java create mode 100644 src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java create mode 100644 src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java create mode 100644 src/main/java/org/jenkinsci/plugins/docker/workflow/client/LaunchResult.java create mode 100644 src/main/resources/index.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/docker/workflow/Docker.groovy create mode 100644 src/main/resources/org/jenkinsci/plugins/docker/workflow/DockerDSL/help.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/docker/workflow/FromFingerprintStep/config.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/docker/workflow/FromFingerprintStep/help-dockerfile.html create mode 100644 src/main/resources/org/jenkinsci/plugins/docker/workflow/FromFingerprintStep/help-image.html create mode 100644 src/main/resources/org/jenkinsci/plugins/docker/workflow/FromFingerprintStep/help.html create mode 100644 src/main/resources/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStep/config.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStep/help.html create mode 100644 src/main/resources/org/jenkinsci/plugins/docker/workflow/RunFingerprintStep/config.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/docker/workflow/RunFingerprintStep/help.html create mode 100644 src/main/resources/org/jenkinsci/plugins/docker/workflow/ServerEndpointStep/config.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/docker/workflow/ServerEndpointStep/help.html create mode 100644 src/main/resources/org/jenkinsci/plugins/docker/workflow/WithContainerStep/config.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/docker/workflow/WithContainerStep/help-args.html create mode 100644 src/main/resources/org/jenkinsci/plugins/docker/workflow/WithContainerStep/help.html create mode 100644 src/test/java/org/jenkinsci/plugins/docker/workflow/DockerDSLTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/docker/workflow/DockerTestUtil.java create mode 100644 src/test/java/org/jenkinsci/plugins/docker/workflow/ImageNameTokensTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStepTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/docker/workflow/ServerEndpointStepTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/docker/workflow/WithContainerStepTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/docker/workflow/client/DockerClientTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..c3f662590 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +target +work + +demo/Dockerfile +demo/.cache/ +# TODO: remove once this plugin is released +demo/JENKINS_HOME/plugins/ diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000..a33530ff7 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,20 @@ +The MIT License + +Copyright (c) 2015, CloudBees, 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. diff --git a/README.md b/README.md new file mode 100644 index 000000000..b5022c8d1 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +CloudBees Docker Workflow Plugin +===================================== + +Jenkins plugin, which allows building and using Docker containers from workflows. + +Summary +--- +The plugin provides: +* DSL commands for building and running Docker images +* Wrapper blocks defining Docker server and registry endpoints +* Actions displaying images related to the build (to be merged with docker-commons) +* etc. + +More info is available on the plugin's [Wiki page](http://wiki.jenkins-ci.org/display/JENKINS/CloudBees+Docker+Workflow+Plugin) + +Demo +--- +The plugin has a Docker-based demo. See the [demo README](demo/README.md) page for setup and launch guidelines. + +License +--- +[MIT License](http://opensource.org/licenses/MIT) diff --git a/demo/.dockerignore b/demo/.dockerignore new file mode 100644 index 000000000..16d3c4dbb --- /dev/null +++ b/demo/.dockerignore @@ -0,0 +1 @@ +.cache diff --git a/demo/Dockerfile_release b/demo/Dockerfile_release new file mode 100644 index 000000000..037d526df --- /dev/null +++ b/demo/Dockerfile_release @@ -0,0 +1,59 @@ +## +# The MIT License +# +# Copyright (c) 2015, CloudBees, 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. +## + +FROM jenkinsci/workflow-demo:1.7-alpha-1 + +############################################################### +# Docker in Docker https://github.com/jpetazzo/dind + +# Install Docker from Docker Inc. repositories. +RUN curl -sSL https://get.docker.com/ubuntu/ | sh + +# Install the magic wrapper. +ADD wrapdocker /usr/local/bin/wrapdocker +RUN chmod +x /usr/local/bin/wrapdocker + +# Define additional metadata for our image. +VOLUME /var/lib/docker + +# +############################################################### + +COPY workflow-reg-proxy.conf /tmp/files/regup/workflow-reg-proxy.conf +COPY gen-security-data.sh /usr/local/bin/gen-security-data.sh +RUN /usr/local/bin/gen-security-data.sh /tmp/files/regup/sec +RUN apt-get install -y apparmor +COPY run-demo.sh /usr/local/bin/run-demo.sh + +COPY plugins.txt /tmp/files/ +RUN /usr/local/bin/plugins.sh /tmp/files/plugins.txt +RUN touch /usr/share/jenkins/ref/plugins/credentials.jpi.pinned + +ADD JENKINS_HOME /usr/share/jenkins/ref + +# TODO until https://github.com/jenkinsci/docker/pull/89 is picked up upstream, shut up please: +RUN perl -i -p -e 's/ *echo.+//g' /usr/local/bin/jenkins.sh + +# wrapdocker has been modified to launch Jenkins via the installed run.sh script +CMD ["wrapdocker"] diff --git a/demo/Dockerfile_snapshot b/demo/Dockerfile_snapshot new file mode 100644 index 000000000..fb478cae2 --- /dev/null +++ b/demo/Dockerfile_snapshot @@ -0,0 +1,60 @@ +## +# The MIT License +# +# Copyright (c) 2015, CloudBees, 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. +## + +FROM jenkinsci/workflow-demo:SNAPSHOT + +############################################################### +# Docker in Docker https://github.com/jpetazzo/dind + +# Install Docker from Docker Inc. repositories. +RUN curl -sSL https://get.docker.com/ubuntu/ | sh + +# Install the magic wrapper. +ADD wrapdocker /usr/local/bin/wrapdocker +RUN chmod +x /usr/local/bin/wrapdocker + +# Define additional metadata for our image. +VOLUME /var/lib/docker + +# +############################################################### + +RUN apt-get install -y apparmor apache2-utils + +COPY workflow-reg-proxy.conf /tmp/files/regup/workflow-reg-proxy.conf +COPY gen-security-data.sh /usr/local/bin/gen-security-data.sh +RUN /usr/local/bin/gen-security-data.sh /tmp/files/regup/sec +COPY run-demo.sh /usr/local/bin/run-demo.sh + +COPY plugins.txt /tmp/files/ +RUN /usr/local/bin/plugins.sh /tmp/files/plugins.txt +RUN touch /usr/share/jenkins/ref/plugins/credentials.jpi.pinned + +ADD JENKINS_HOME /usr/share/jenkins/ref + +# TODO until https://github.com/jenkinsci/docker/pull/89 is picked up upstream, shut up please: +RUN perl -i -p -e 's/ *echo.+//g' /usr/local/bin/jenkins.sh + +# wrapdocker has been modified to launch Jenkins via the installed run.sh script +CMD ["wrapdocker"] diff --git a/demo/JENKINS_HOME/config.xml b/demo/JENKINS_HOME/config.xml new file mode 100644 index 000000000..1aa01d221 --- /dev/null +++ b/demo/JENKINS_HOME/config.xml @@ -0,0 +1,34 @@ + + + + 1.0 + 2 + NORMAL + true + + + false + + ${ITEM_ROOTDIR}/workspace + ${ITEM_ROOTDIR}/builds + + + + + + 0 + + + + All + false + false + + + + All + 0 + + + + diff --git a/demo/JENKINS_HOME/credentials.xml b/demo/JENKINS_HOME/credentials.xml new file mode 100644 index 000000000..8b90faca4 --- /dev/null +++ b/demo/JENKINS_HOME/credentials.xml @@ -0,0 +1,19 @@ + + + + + + + + + + GLOBAL + docker-registry-login + + workflowuser + 123123123 + + + + + diff --git a/demo/JENKINS_HOME/jobs/docker-workflow/config.xml b/demo/JENKINS_HOME/jobs/docker-workflow/config.xml new file mode 100644 index 000000000..c388e4fae --- /dev/null +++ b/demo/JENKINS_HOME/jobs/docker-workflow/config.xml @@ -0,0 +1,72 @@ + + + + Demonstrates Jenkins Workflow integration with Docker based on +<a href="https://wiki.jenkins-ci.org/display/JENKINS/CloudBees+Docker+Workflow+Plugin">CloudBees Docker Workflow</a> plugin + + false + + + + true + + + diff --git a/demo/Makefile b/demo/Makefile new file mode 100644 index 000000000..4d1038fa2 --- /dev/null +++ b/demo/Makefile @@ -0,0 +1,73 @@ +## +# The MIT License +# +# Copyright (c) 2015, CloudBees, 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. +## +TAG=$(shell cat workflow-version.txt) +IMAGE=jenkinsci/docker-workflow-demo +CACHE=$(shell pwd)/.cache + +# +# Build from the Dockerfile_release Dockerfile +# Only run this target when the project pom dependencies are RELEASED dependencies. +# +# TODO: Remove and delete ./get-snapshots.sh once this plugin is released. See comment in ./get-snapshots.sh +# +build: + ./get-snapshots.sh + docker build -t $(IMAGE):$(TAG) -f Dockerfile_release . + +# +# Build from the Dockerfile_release Dockerfile +# Only run this target when the project pom dependencies are SNAPSHOT dependencies. +# +# TODO: Remove and delete ./get-snapshots.sh once this plugin is released. See comment in ./get-snapshots.sh +# +build-snapshot: + ./get-snapshots.sh + docker build -t $(IMAGE):SNAPSHOT -f Dockerfile_snapshot . + +# +# To connect a Java debugger to the Jenkins instance running in the docker container, simply add the following +# options to the "docker run" command (just after the port mappings): +# +# -p 5500:5500 -e JAVA_OPTS=-Xrunjdwp:transport=dt_socket,server=y,address=5500,suspend=n +# +# If using boot2docker, you need to tell your remote debugger to use the boot2docker VM ip (ala boot2docker ip). +# + +run: + docker run --rm -p 8080:8080 -p 8081:8081 -p 8022:22 -p 18080:18080 --add-host=docker.example.com:127.0.0.1 -ti --privileged $(IMAGE):$(TAG) + +run-snapshot: + docker run --rm -p 8080:8080 -p 8081:8081 -p 8022:22 -p 18080:18080 --add-host=docker.example.com:127.0.0.1 -ti --privileged $(IMAGE):SNAPSHOT + +run-snapshot-cached: + mkdir -p $(CACHE)/docker + sudo rm -rf $(CACHE)/docker/containers $(CACHE)/docker/linkgraph.db + mkdir -p $(CACHE)/maven + docker run --rm -p 8080:8080 -p 8081:8081 -p 8022:22 -p 18080:18080 --add-host=docker.example.com:127.0.0.1 -ti --privileged -v $(CACHE)/docker:/var/lib/docker -v $(CACHE)/maven:/m2repo $(IMAGE):SNAPSHOT + +clean: + rm -rf JENKINS_HOME/plugins + +push: + docker push $(IMAGE):SNAPSHOT diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 000000000..8d6385642 --- /dev/null +++ b/demo/README.md @@ -0,0 +1,31 @@ +Docker image for Docker workflow demo +===================================== +This image contains a "Docker Workflow" Job that demonstrates Jenkins Workflow integration +with Docker via [CloudBees Docker Workflow](https://wiki.jenkins-ci.org/display/JENKINS/CloudBees+Docker+Workflow+Plugin) plugin. + +``` +docker run --rm -p 8080:8080 -p 8081:8081 -p 8022:22 --add-host=docker.example.com:127.0.0.1 -ti --privileged jenkinsci/docker-workflow-demo +``` + +The "Docker Workflow" Job simply does the following: + +1. Gets the Spring Pet Clinic demonstration application code from GitHub. +1. Builds the Pet Clinic application in a Docker container. +1. Builds a runnable Pet Clinic application Docker image. +1. Runs a Pet Clinic app container (from the Pet Clinic application Docker image) + a second maven3 container that runs automated tests against the Pet Clinic app container. + * The 2 containers are linked, allowing the test container to fire requests at the Pet Clinic app container. + +The "Docker Workflow" Job demonstrates how to use the `docker` DSL: + +1. Use `docker.image` to define a DSL `Image` object (not to be confused with `build`) that can then be used to perform operations on a Docker image: + * use `Image.inside` to run a Docker container and execute commands in it. The build workspace is mounted as the working directory in the container. + * use `Image.run` to run a Docker container in detached mode, returning a DSL `Container` object that can be later used to stop the container (via `Container.stop`). +1. Use `docker.build` to build a Docker image from a `Dockerfile`, returning a DSL `Image` object that can then be used to perform operations on that image (as above). + +The `docker` DSL supports some additional capabilities not shown in the "Docker Workflow" Job: + +1. Use the `docker.withRegistry` and `docker.withServer` to register endpoints for the Docker registry and host to be used when executing docker commands. + * `docker.withRegistry(, )` + * `docker.withServer(, )` +1. Use the `Image.pull` to pull Docker image layers into the Docker host cache. +1. Use the `Image.push` to push a Docker image to the associated Docker Registry. See `docker.withRegistry` above. diff --git a/demo/gen-security-data.sh b/demo/gen-security-data.sh new file mode 100755 index 000000000..a1416154c --- /dev/null +++ b/demo/gen-security-data.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +## +# The MIT License +# +# Copyright (c) 2015, CloudBees, 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. +## +rm -rf $1 +mkdir -p $1 + +pushd $1 + +htpasswd -bmc docker-registry.htpasswd workflowuser 123123123 + +# Create the CA Key and Certificate for signing Certs +openssl genrsa -des3 -passout pass:x -out ca.key 4096 +openssl rsa -passin pass:x -in ca.key -out ca.key # remove password! +openssl req -new -x509 -days 365 -key ca.key -out ca.crt -subj "/C=US/ST=California/L=San Jose/O=Jenkins CI/OU=Workflow Dept/CN=docker.example.com" + +# Create the Server Key, CSR, and Certificate +openssl genrsa -des3 -passout pass:x -out key.pem 1024 +openssl rsa -passin pass:x -in key.pem -out key.pem # remove password! +openssl req -new -key key.pem -out server.csr -subj "/C=US/ST=California/L=San Jose/O=Jenkins CI/OU=Workflow Dept/CN=docker.example.com" + +# Self sign the server cert. +openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out cert.pem + +# cat the ca cert onto the server cert +cat ca.crt >> cert.pem + +# White-list the CA cert (because it is self-signed), otherwise docker client will not be able to authenticate +cp ca.crt /usr/local/share/ca-certificates +update-ca-certificates + +popd diff --git a/demo/get-snapshots.sh b/demo/get-snapshots.sh new file mode 100755 index 000000000..fc32d3289 --- /dev/null +++ b/demo/get-snapshots.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +## +# The MIT License +# +# Copyright (c) 2015, CloudBees, 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. +## + +# +# TODO: This is temp (resurrected from earlier). Will be replaced by an entry in plugins.txt once we release this plugin. Then delete this script. +# + +PROJECT_DIR=.. +PLUGIN_HPI=$PROJECT_DIR/target/docker-workflow.hpi + +if [[ ! -f $PLUGIN_HPI ]]; then + # Build the plugin + pushd $PROJECT_DIR + mvn clean install -DskipTests + popd +fi + +# Cleanup JENKINS_HOME +PLUGINS_DIR=./JENKINS_HOME/plugins +rm -rf $PLUGINS_DIR +mkdir -p $PLUGINS_DIR + +# Copy and pin this project plugin +cp $PLUGIN_HPI $PLUGINS_DIR + +# Rename and pin all plugins. +pushd $PLUGINS_DIR +for hpiFile in *.hpi +do + prefix=$(echo $hpiFile | sed 's/\(.*\)\.hpi/\1/') + mv $hpiFile $prefix.jpi + touch $prefix.jpi.pinned +done +popd + + diff --git a/demo/plugins.txt b/demo/plugins.txt new file mode 100644 index 000000000..97618dd0d --- /dev/null +++ b/demo/plugins.txt @@ -0,0 +1,3 @@ +docker-commons:1.0 +authentication-tokens:1.1 +credentials:1.22 diff --git a/demo/run-demo.sh b/demo/run-demo.sh new file mode 100755 index 000000000..4db015134 --- /dev/null +++ b/demo/run-demo.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +## +# The MIT License +# +# Copyright (c) 2015, CloudBees, 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. +## + +# +# Install a private registry that can be used by the demo to push images to. +# + +echo '*************** Installing a local Docker Registry Service for the demo ***************' +echo '*************** Please sit tight for a minute ***************' + +REG_SETUP_PATH=/tmp/files/regup + +docker pull registry:0.9.1 +docker run -d --name registry registry:0.9.1 +docker pull nginx:1.9.0 +docker run -d -p 443:443 --name wf-registry-proxy -v $REG_SETUP_PATH:/etc/nginx/conf.d/ -v $REG_SETUP_PATH/sec:/var/registry/certs --link registry:registry nginx:1.9.0 + +echo '*************** Docker Registry Service running now ***************' + +# In case some tagged images were left over from a previous run using a cache: +(docker images -q examplecorp/spring-petclinic; docker images -q docker.example.com/examplecorp/spring-petclinic) | xargs docker rmi --no-prune=true --force + +# +# Remove the base workflow-demo "cd" job +# +rm -rf /usr/share/jenkins/ref/jobs/cd /var/jenkins_home/jobs/cd + +# +# Now run Jenkins. +# +# +/usr/local/bin/run.sh diff --git a/demo/workflow-reg-proxy.conf b/demo/workflow-reg-proxy.conf new file mode 100644 index 000000000..9b8a12c7c --- /dev/null +++ b/demo/workflow-reg-proxy.conf @@ -0,0 +1,37 @@ +server { + listen 443 ssl; + server_name docker.example.com; + + ssl on; + ssl_certificate /var/registry/certs/cert.pem; + ssl_certificate_key /var/registry/certs/key.pem; + ssl_verify_client off; + + proxy_set_header Host $http_host; # required for Docker client + proxy_set_header X-Real-IP $remote_addr; # pass client IP + proxy_set_header Authorization ""; # see https://github.com/dotcloud/docker-registry/issues/170 + proxy_read_timeout 900; + + proxy_set_header X-Forwarded-Proto "https"; + proxy_set_header X-Forwarded-Protocol "https"; + + client_max_body_size 0; # disable any limits to avoid HTTP 413 for large image uploads + + # required to avoid HTTP 411: see Issue #1486 (https://github.com/dotcloud/docker/issues/1486) + chunked_transfer_encoding on; + + location / { + # let Nginx know about our auth file + auth_basic "Restricted Docker Registry"; + auth_basic_user_file /var/registry/certs/docker-registry.htpasswd; + proxy_pass http://registry:5000/; + } + location /_ping { + auth_basic off; + proxy_pass http://registry:5000/; + } + location /v1/_ping { + auth_basic off; + proxy_pass http://registry:5000/; + } +} diff --git a/demo/workflow-version.txt b/demo/workflow-version.txt new file mode 100644 index 000000000..fc78a494e --- /dev/null +++ b/demo/workflow-version.txt @@ -0,0 +1 @@ +1.7-alpha-1 diff --git a/demo/wrapdocker b/demo/wrapdocker new file mode 100755 index 000000000..1c2ef3f9b --- /dev/null +++ b/demo/wrapdocker @@ -0,0 +1,137 @@ +#!/bin/bash + +## +# The MIT License +# +# Copyright (c) 2015, CloudBees, 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. +## + +# Ensure that all nodes in /dev/mapper correspond to mapped devices currently loaded by the device-mapper kernel driver +dmsetup mknodes + +# First, make sure that cgroups are mounted correctly. +CGROUP=/sys/fs/cgroup +: {LOG:=stdio} + +[ -d $CGROUP ] || + mkdir $CGROUP + +mountpoint -q $CGROUP || + mount -n -t tmpfs -o uid=0,gid=0,mode=0755 cgroup $CGROUP || { + echo "Could not make a tmpfs mount. Did you use --privileged?" + exit 1 + } + +if [ -d /sys/kernel/security ] && ! mountpoint -q /sys/kernel/security +then + mount -t securityfs none /sys/kernel/security || { + echo "Could not mount /sys/kernel/security." + echo "AppArmor detection and --privileged mode might break." + } +fi + +# Mount the cgroup hierarchies exactly as they are in the parent system. +for SUBSYS in $(cut -d: -f2 /proc/1/cgroup) +do + [ -d $CGROUP/$SUBSYS ] || mkdir $CGROUP/$SUBSYS + mountpoint -q $CGROUP/$SUBSYS || + mount -n -t cgroup -o $SUBSYS cgroup $CGROUP/$SUBSYS + + # The two following sections address a bug which manifests itself + # by a cryptic "lxc-start: no ns_cgroup option specified" when + # trying to start containers withina container. + # The bug seems to appear when the cgroup hierarchies are not + # mounted on the exact same directories in the host, and in the + # container. + + # Named, control-less cgroups are mounted with "-o name=foo" + # (and appear as such under /proc//cgroup) but are usually + # mounted on a directory named "foo" (without the "name=" prefix). + # Systemd and OpenRC (and possibly others) both create such a + # cgroup. To avoid the aforementioned bug, we symlink "foo" to + # "name=foo". This shouldn't have any adverse effect. + echo $SUBSYS | grep -q ^name= && { + NAME=$(echo $SUBSYS | sed s/^name=//) + ln -s $SUBSYS $CGROUP/$NAME + } + + # Likewise, on at least one system, it has been reported that + # systemd would mount the CPU and CPU accounting controllers + # (respectively "cpu" and "cpuacct") with "-o cpuacct,cpu" + # but on a directory called "cpu,cpuacct" (note the inversion + # in the order of the groups). This tries to work around it. + [ $SUBSYS = cpuacct,cpu ] && ln -s $SUBSYS $CGROUP/cpu,cpuacct +done + +# Note: as I write those lines, the LXC userland tools cannot setup +# a "sub-container" properly if the "devices" cgroup is not in its +# own hierarchy. Let's detect this and issue a warning. +grep -q :devices: /proc/1/cgroup || + echo "WARNING: the 'devices' cgroup should be in its own hierarchy." +grep -qw devices /proc/1/cgroup || + echo "WARNING: it looks like the 'devices' cgroup is not mounted." + +# Now, close extraneous file descriptors. +pushd /proc/self/fd >/dev/null +for FD in * +do + case "$FD" in + # Keep stdin/stdout/stderr + [012]) + ;; + # Nuke everything else + *) + eval exec "$FD>&-" + ;; + esac +done +popd >/dev/null + + +# If a pidfile is still around (for example after a container restart), +# delete it so that docker can start. +rm -rf /var/run/docker.pid + +# If we were given a PORT environment variable, start as a simple daemon; +# otherwise, spawn a shell as well +if [ "$PORT" ] +then + exec docker -d -H 0.0.0.0:$PORT -H unix:///var/run/docker.sock \ + $DOCKER_DAEMON_ARGS +else + if [ "$LOG" == "file" ] + then + docker -d $DOCKER_DAEMON_ARGS &>/var/log/docker.log & + else + docker -d $DOCKER_DAEMON_ARGS & + fi + (( timeout = 60 + SECONDS )) + until docker info >/dev/null 2>&1 + do + if (( SECONDS >= timeout )); then + echo 'Timed out trying to connect to internal docker host.' >&2 + break + fi + sleep 1 + done + [[ $1 ]] && exec "$@" + exec bash --login -c /usr/local/bin/run-demo.sh +fi diff --git a/pom.xml b/pom.xml new file mode 100644 index 000000000..09a13d5c1 --- /dev/null +++ b/pom.xml @@ -0,0 +1,106 @@ + + + 4.0.0 + + org.jenkins-ci.plugins + plugin + 1.596.1 + + docker-workflow + 1.0-alpha-1-SNAPSHOT + CloudBees Docker Workflow + Build and use Docker containers from workflows. + hpi + + + + MIT License + http://opensource.org/licenses/MIT + + + + http://wiki.jenkins-ci.org/display/JENKINS/CloudBees+Docker+Workflow+Plugin + + scm:git:git://github.com/jenkinsci/${project.artifactId}-plugin.git + scm:git:git@github.com:jenkinsci/${project.artifactId}-plugin.git + https://github.com/jenkinsci/${project.artifactId}-plugin + + + + 1.7-alpha-1 + 1.112 + + + + repo.jenkins-ci.org + http://repo.jenkins-ci.org/public/ + + + + + repo.jenkins-ci.org + http://repo.jenkins-ci.org/public/ + + + + + org.jenkins-ci.plugins + docker-commons + 1.0 + + + org.jenkins-ci.plugins + script-security + 1.14 + + + org.jenkins-ci.plugins.workflow + workflow-step-api + ${workflow.version} + + + org.jenkins-ci.plugins.workflow + workflow-cps + ${workflow.version} + + + org.jenkins-ci.plugins.workflow + workflow-aggregator + ${workflow.version} + test + + + org.jenkins-ci.plugins.workflow + workflow-aggregator + tests + ${workflow.version} + test + + + org.jenkins-ci.plugins.workflow + workflow-step-api + tests + ${workflow.version} + test + + + org.jenkins-ci.plugins.workflow + workflow-support + tests + ${workflow.version} + test + + + org.jenkins-ci.modules + sshd + 1.6 + test + + + org.jenkins-ci.plugins.workflow + workflow-durable-task-step + ${workflow.version} + test + + + diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/AbstractEndpointStepExecution.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/AbstractEndpointStepExecution.java new file mode 100644 index 000000000..eb88e17c7 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/AbstractEndpointStepExecution.java @@ -0,0 +1,101 @@ +/* + * The MIT License + * + * Copyright (c) 2015, CloudBees, 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. + */ +package org.jenkinsci.plugins.docker.workflow; + +import hudson.EnvVars; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.jenkinsci.plugins.docker.commons.credentials.KeyMaterial; +import org.jenkinsci.plugins.docker.commons.credentials.KeyMaterialFactory; +import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl; +import org.jenkinsci.plugins.workflow.steps.BodyExecutionCallback; +import org.jenkinsci.plugins.workflow.steps.EnvironmentExpander; +import org.jenkinsci.plugins.workflow.steps.StepContext; + +abstract class AbstractEndpointStepExecution extends AbstractStepExecutionImpl { + + private static final long serialVersionUID = 1; + + protected abstract KeyMaterialFactory newKeyMaterialFactory() throws IOException, InterruptedException; + + @Override public final boolean start() throws Exception { + KeyMaterialFactory keyMaterialFactory = newKeyMaterialFactory(); + KeyMaterial material = keyMaterialFactory.materialize(); + getContext().newBodyInvoker(). + withContext(EnvironmentExpander.merge(getContext().get(EnvironmentExpander.class), new Expander(material))). + withCallback(new Callback(material)). + start(); + return false; + } + + @Override public final void stop(Throwable cause) throws Exception { + // should not need to do anything special + } + + private static class Expander extends EnvironmentExpander { + + private static final long serialVersionUID = 1; + private final KeyMaterial material; + + Expander(KeyMaterial material) { + this.material = material; + } + + @Override public void expand(EnvVars env) throws IOException, InterruptedException { + env.putAll(material.env()); + } + + } + + private static class Callback extends BodyExecutionCallback { + + private static final long serialVersionUID = 1; + private final KeyMaterial material; + + Callback(KeyMaterial material) { + this.material = material; + } + + private void close() { + try { + material.close(); + } catch (IOException x) { + Logger.getLogger(AbstractEndpointStepExecution.class.getName()).log(Level.WARNING, null, x); + } + } + + @Override public void onSuccess(StepContext context, Object result) { + close(); + context.onSuccess(result); + } + + @Override public void onFailure(StepContext context, Throwable t) { + close(); + context.onFailure(t); + } + + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/DockerDSL.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/DockerDSL.java new file mode 100644 index 000000000..608303e32 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/DockerDSL.java @@ -0,0 +1,67 @@ +/* + * The MIT License + * + * Copyright (c) 2015, CloudBees, 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. + */ +package org.jenkinsci.plugins.docker.workflow; + +import groovy.lang.Binding; +import hudson.Extension; +import java.io.IOException; +import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.ProxyWhitelist; +import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.StaticWhitelist; +import org.jenkinsci.plugins.workflow.cps.CpsScript; +import org.jenkinsci.plugins.workflow.cps.GlobalVariable; + +@Extension public class DockerDSL extends GlobalVariable { + + @Override public String getName() { + return "docker"; + } + + @Override public Object getValue(CpsScript script) throws Exception { + Binding binding = script.getBinding(); + Object docker; + if (binding.hasVariable(getName())) { + docker = binding.getVariable(getName()); + } else { + // Note that if this were a method rather than a constructor, we would need to mark it @NonCPS lest it throw CpsCallableInvocation. + docker = script.getClass().getClassLoader().loadClass("org.jenkinsci.plugins.docker.workflow.Docker").getConstructor(CpsScript.class).newInstance(script); + binding.setVariable(getName(), docker); + } + return docker; + } + + @Extension public static class MiscWhitelist extends ProxyWhitelist { // TODO things that ought to be in script-security or docker-commons + public MiscWhitelist() throws IOException { + super(new StaticWhitelist( + "method java.util.concurrent.Callable call", + "method groovy.lang.Closure call java.lang.Object", + "method java.lang.Object toString", + "method java.lang.String trim", + "new org.jenkinsci.plugins.docker.commons.credentials.DockerRegistryEndpoint java.lang.String java.lang.String", + "method org.jenkinsci.plugins.docker.commons.credentials.DockerRegistryEndpoint imageName java.lang.String", + "staticMethod org.codehaus.groovy.runtime.ScriptBytecodeAdapter compareNotEqual java.lang.Object java.lang.Object", + "staticMethod org.codehaus.groovy.runtime.ScriptBytecodeAdapter compareEqual java.lang.Object java.lang.Object")); + } + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/FromFingerprintStep.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/FromFingerprintStep.java new file mode 100644 index 000000000..26f689ecf --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/FromFingerprintStep.java @@ -0,0 +1,140 @@ +/* + * The MIT License + * + * Copyright (c) 2015, CloudBees, 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. + */ +package org.jenkinsci.plugins.docker.workflow; + +import org.jenkinsci.plugins.docker.workflow.client.DockerClient; +import com.google.inject.Inject; +import hudson.AbortException; +import hudson.EnvVars; +import hudson.Extension; +import hudson.FilePath; +import hudson.Launcher; +import hudson.Util; +import hudson.model.Node; +import hudson.model.Run; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import org.jenkinsci.plugins.docker.commons.fingerprint.DockerFingerprints; +import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; +import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; +import org.jenkinsci.plugins.workflow.steps.AbstractSynchronousStepExecution; +import org.jenkinsci.plugins.workflow.steps.StepContextParameter; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; + +public class FromFingerprintStep extends AbstractStepImpl { + + private final String dockerfile; + private final String image; + private String toolName; + + @DataBoundConstructor public FromFingerprintStep(String dockerfile, String image) { + this.dockerfile = dockerfile; + this.image = image; + } + + public String getDockerfile() { + return dockerfile; + } + + public String getImage() { + return image; + } + + public String getToolName() { + return toolName; + } + + @DataBoundSetter public void setToolName(String toolName) { + this.toolName = Util.fixEmpty(toolName); + } + + public static class Execution extends AbstractSynchronousStepExecution { + + @Inject(optional=true) private transient FromFingerprintStep step; + @SuppressWarnings("rawtypes") // TODO not compiling on cloudbees.ci + @StepContextParameter private transient Run run; + @StepContextParameter private transient Launcher launcher; + @StepContextParameter private transient EnvVars env; + @StepContextParameter private transient FilePath workspace; + @StepContextParameter private transient Node node; + + @Override protected Void run() throws Exception { + String fromImage = null; + FilePath dockerfile = workspace.child(step.dockerfile); + InputStream is = dockerfile.read(); + try { + BufferedReader r = new BufferedReader(new InputStreamReader(is)); // encoding probably irrelevant since image/tag names must be ASCII + String line; + while ((line = r.readLine()) != null) { + line = line.trim(); + if (line.startsWith("#")) { + continue; + } + if (line.startsWith("FROM ")) { + fromImage = line.substring(5); + break; + } + } + } finally { + is.close(); + } + if (fromImage == null) { + throw new AbortException("could not find FROM instruction in " + dockerfile); + } + DockerClient client = new DockerClient(launcher, node, step.toolName); + String descendantImageId = client.inspect(env, step.image, ".Id"); + if (fromImage.equals("scratch")) { // we just made a base image + DockerFingerprints.addFromFacet(null, descendantImageId, run); + } else { + DockerFingerprints.addFromFacet(client.inspect(env, fromImage, ".Id"), descendantImageId, run); + ImageAction.add(fromImage, run); + } + return null; + } + + } + + @Extension public static class DescriptorImpl extends AbstractStepDescriptorImpl { + + public DescriptorImpl() { + super(Execution.class); + } + + @Override public String getFunctionName() { + return "dockerFingerprintFrom"; + } + + @Override public String getDisplayName() { + return "Record trace of a Docker image used in FROM"; + } + + @Override public boolean isAdvanced() { + return true; + } + + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/ImageAction.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/ImageAction.java new file mode 100644 index 000000000..b2a4d4c19 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/ImageAction.java @@ -0,0 +1,90 @@ +/* + * The MIT License + * + * Copyright (c) 2015, CloudBees, 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. + */ +package org.jenkinsci.plugins.docker.workflow; + +import hudson.BulkChange; +import hudson.Extension; +import hudson.model.InvisibleAction; +import hudson.model.Job; +import hudson.model.Run; +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import java.util.TreeSet; +import org.jenkinsci.plugins.docker.commons.DockerImageExtractor; +import org.jenkinsci.plugins.docker.commons.fingerprint.DockerFingerprintAction; + +/** + * Represents that a build used a particular Docker image. + * Unlike {@link DockerFingerprintAction} this records the actual name, not an image ID. + */ +final class ImageAction extends InvisibleAction /* implements RunAction2 */ { + + private final Set names = new TreeSet(); + + ImageAction() {} + + /** + * Records an image name as per {@code Config.Image} in {@code docker inspect $container} or a {@code FROM} instruction. + * Typically in {@code repository} or {@code user/repository} format, but may include tags {@code repo:latest} or hashes {@code repo@123abc}. + * @see this specification which does not really specify anything + */ + static void add(String image, Run run) throws IOException { + synchronized (run) { + BulkChange bc = new BulkChange(run); + try { + ImageAction action = run.getAction(ImageAction.class); + if (action == null) { + action = new ImageAction(); + run.addAction(action); + } + action.names.add(image); + bc.commit(); + } finally { + bc.abort(); + } + } + } + + @Extension public static final class ExtractorImpl extends DockerImageExtractor { + + @Override public Collection getDockerImagesUsedByJob(Job job) { + Run build = job.getLastCompletedBuild(); + if (build != null) { + ImageAction action = build.getAction(ImageAction.class); + if (action != null) { + Set bareNames = new TreeSet(); + for (String name : action.names) { + bareNames.add(name./* strip any tag or hash */replaceFirst("[:@].+", "")); + } + return bareNames; + } + } + return Collections.emptySet(); + } + + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/ImageNameTokens.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/ImageNameTokens.java new file mode 100644 index 000000000..0a627ef8f --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/ImageNameTokens.java @@ -0,0 +1,65 @@ +/* + * The MIT License + * + * Copyright (c) 2015, CloudBees, 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. + */ +package org.jenkinsci.plugins.docker.workflow; + +import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted; + +import javax.annotation.Nonnull; +import java.io.Serializable; + +/** + * Image name. + * + *

+ * Not including the registry host name. + * + * TODO: Should this be in docker-commons? + * + * @author tom.fennelly@gmail.com + */ +public class ImageNameTokens implements Serializable { + + private static final long serialVersionUID = 1L; + + @Whitelisted + public final String userAndRepo; + @Whitelisted + public final String tag; + + @Whitelisted + public ImageNameTokens(@Nonnull String name) { + int tagIdx = name.lastIndexOf(':'); + if (tagIdx != -1) { + this.userAndRepo = name.substring(0, tagIdx); + if (tagIdx < name.length() - 1) { + this.tag = name.substring(tagIdx + 1); + } else { + this.tag = "latest"; + } + } else { + this.userAndRepo = name; + this.tag = "latest"; + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStep.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStep.java new file mode 100644 index 000000000..4a6ccf6af --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStep.java @@ -0,0 +1,90 @@ +/* + * The MIT License + * + * Copyright (c) 2015, CloudBees, 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. + */ +package org.jenkinsci.plugins.docker.workflow; + +import com.google.inject.Inject; +import hudson.Extension; +import hudson.FilePath; +import hudson.model.Job; +import java.io.IOException; +import javax.annotation.Nonnull; +import org.jenkinsci.plugins.docker.commons.credentials.DockerRegistryEndpoint; +import org.jenkinsci.plugins.docker.commons.credentials.KeyMaterialFactory; +import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; +import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; +import org.jenkinsci.plugins.workflow.steps.StepContextParameter; +import org.kohsuke.stapler.DataBoundConstructor; + +public class RegistryEndpointStep extends AbstractStepImpl { + + private final @Nonnull DockerRegistryEndpoint registry; + + @DataBoundConstructor public RegistryEndpointStep(@Nonnull DockerRegistryEndpoint registry) { + assert registry != null; + this.registry = registry; + } + + public DockerRegistryEndpoint getRegistry() { + return registry; + } + + public static class Execution extends AbstractEndpointStepExecution { + + private static final long serialVersionUID = 1; + + @Inject(optional=true) private transient RegistryEndpointStep step; + @StepContextParameter private transient Job job; + @StepContextParameter private transient FilePath workspace; + + @Override protected KeyMaterialFactory newKeyMaterialFactory() throws IOException, InterruptedException { + return step.registry.newKeyMaterialFactory(job, workspace.getChannel()); + } + + } + + @Extension public static class DescriptorImpl extends AbstractStepDescriptorImpl { + + public DescriptorImpl() { + super(Execution.class); + } + + @Override public String getFunctionName() { + return "withDockerRegistry"; + } + + @Override public String getDisplayName() { + return "Sets up Docker registry endpoint"; + } + + @Override public boolean takesImplicitBlockArgument() { + return true; + } + + @Override public boolean isAdvanced() { + return true; + } + + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/RunFingerprintStep.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/RunFingerprintStep.java new file mode 100644 index 000000000..d1204b7a9 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/RunFingerprintStep.java @@ -0,0 +1,105 @@ +/* + * The MIT License + * + * Copyright (c) 2015, CloudBees, 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. + */ +package org.jenkinsci.plugins.docker.workflow; + +import org.jenkinsci.plugins.docker.workflow.client.DockerClient; +import com.google.inject.Inject; +import hudson.EnvVars; +import hudson.Extension; +import hudson.Launcher; +import hudson.Util; +import hudson.model.Node; +import hudson.model.Run; +import org.jenkinsci.plugins.docker.commons.fingerprint.DockerFingerprints; +import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; +import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; +import org.jenkinsci.plugins.workflow.steps.AbstractSynchronousStepExecution; +import org.jenkinsci.plugins.workflow.steps.StepContextParameter; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; + +public class RunFingerprintStep extends AbstractStepImpl { + + private final String containerId; + private String toolName; + + @DataBoundConstructor public RunFingerprintStep(String containerId) { + this.containerId = containerId; + } + + public String getContainerId() { + return containerId; + } + + public String getToolName() { + return toolName; + } + + @DataBoundSetter public void setToolName(String toolName) { + this.toolName = Util.fixEmpty(toolName); + } + + public static class Execution extends AbstractSynchronousStepExecution { + + @Inject(optional=true) private transient RunFingerprintStep step; + @SuppressWarnings("rawtypes") // TODO not compiling on cloudbees.ci + @StepContextParameter private transient Run run; + @StepContextParameter private transient Launcher launcher; + @StepContextParameter private transient EnvVars env; + @StepContextParameter private transient Node node; + + @SuppressWarnings("SynchronizeOnNonFinalField") // run is quasi-final + @Override protected Void run() throws Exception { + DockerClient client = new DockerClient(launcher, node, step.toolName); + DockerFingerprints.addRunFacet(client.getContainerRecord(env, step.containerId), run); + String image = client.inspect(env, step.containerId, ".Config.Image"); + if (image != null) { + ImageAction.add(image, run); + } + return null; + } + + } + + @Extension public static class DescriptorImpl extends AbstractStepDescriptorImpl { + + public DescriptorImpl() { + super(Execution.class); + } + + @Override public String getFunctionName() { + return "dockerFingerprintRun"; + } + + @Override public String getDisplayName() { + return "Record trace of a Docker image run in a container"; + } + + @Override public boolean isAdvanced() { + return true; + } + + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/ServerEndpointStep.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/ServerEndpointStep.java new file mode 100644 index 000000000..4bbe854bb --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/ServerEndpointStep.java @@ -0,0 +1,90 @@ +/* + * The MIT License + * + * Copyright (c) 2015, CloudBees, 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. + */ +package org.jenkinsci.plugins.docker.workflow; + +import com.google.inject.Inject; +import hudson.Extension; +import hudson.FilePath; +import hudson.model.Job; +import java.io.IOException; +import javax.annotation.Nonnull; +import org.jenkinsci.plugins.docker.commons.credentials.DockerServerEndpoint; +import org.jenkinsci.plugins.docker.commons.credentials.KeyMaterialFactory; +import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; +import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; +import org.jenkinsci.plugins.workflow.steps.StepContextParameter; +import org.kohsuke.stapler.DataBoundConstructor; + +public class ServerEndpointStep extends AbstractStepImpl { + + private final @Nonnull DockerServerEndpoint server; + + @DataBoundConstructor public ServerEndpointStep(@Nonnull DockerServerEndpoint server) { + assert server != null; + this.server = server; + } + + public DockerServerEndpoint getServer() { + return server; + } + + public static class Execution extends AbstractEndpointStepExecution { + + private static final long serialVersionUID = 1; + + @Inject(optional=true) private transient ServerEndpointStep step; + @StepContextParameter private transient Job job; + @StepContextParameter private transient FilePath workspace; + + @Override protected KeyMaterialFactory newKeyMaterialFactory() throws IOException, InterruptedException { + return step.server.newKeyMaterialFactory(job, workspace.getChannel()); + } + + } + + @Extension public static class DescriptorImpl extends AbstractStepDescriptorImpl { + + public DescriptorImpl() { + super(Execution.class); + } + + @Override public String getFunctionName() { + return "withDockerServer"; + } + + @Override public String getDisplayName() { + return "Sets up Docker server endpoint"; + } + + @Override public boolean takesImplicitBlockArgument() { + return true; + } + + @Override public boolean isAdvanced() { + return true; + } + + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java new file mode 100644 index 000000000..fd907447a --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java @@ -0,0 +1,283 @@ +/* + * The MIT License + * + * Copyright (c) 2015, CloudBees, 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. + */ +package org.jenkinsci.plugins.docker.workflow; + +import org.jenkinsci.plugins.docker.workflow.client.DockerClient; +import com.google.inject.Inject; +import hudson.AbortException; +import hudson.EnvVars; +import hudson.Extension; +import hudson.FilePath; +import hudson.Launcher; +import hudson.LauncherDecorator; +import hudson.Proc; +import hudson.Util; +import hudson.model.Computer; +import hudson.model.Node; +import hudson.model.Run; +import hudson.model.TaskListener; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nonnull; + +import hudson.util.VersionNumber; +import org.jenkinsci.plugins.docker.commons.fingerprint.DockerFingerprints; +import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; +import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl; +import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; +import org.jenkinsci.plugins.workflow.steps.BodyExecutionCallback; +import org.jenkinsci.plugins.workflow.steps.BodyInvoker; +import org.jenkinsci.plugins.workflow.steps.StepContext; +import org.jenkinsci.plugins.workflow.steps.StepContextParameter; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; + +public class WithContainerStep extends AbstractStepImpl { + + private static final Logger LOGGER = Logger.getLogger(WithContainerStep.class.getName()); + private final @Nonnull String image; + private String args; + private String toolName; + + @DataBoundConstructor public WithContainerStep(@Nonnull String image) { + this.image = image; + } + + public String getImage() { + return image; + } + + @DataBoundSetter + public void setArgs(String args) { + this.args = Util.fixEmpty(args); + } + + public String getArgs() { + return args; + } + + public String getToolName() { + return toolName; + } + + @DataBoundSetter public void setToolName(String toolName) { + this.toolName = Util.fixEmpty(toolName); + } + + private static void destroy(String container, Launcher launcher, Node node, EnvVars launcherEnv, String toolName) throws Exception { + new DockerClient(launcher, node, toolName).stop(launcherEnv, container); + } + + public static class Execution extends AbstractStepExecutionImpl { + + private static final long serialVersionUID = 1; + @Inject(optional=true) private transient WithContainerStep step; + @StepContextParameter private transient Launcher launcher; + @StepContextParameter private transient TaskListener listener; + @StepContextParameter private transient FilePath workspace; + @StepContextParameter private transient EnvVars env; + @StepContextParameter private transient Computer computer; + @StepContextParameter private transient Node node; + @SuppressWarnings("rawtypes") // TODO not compiling on cloudbees.ci + @StepContextParameter private transient Run run; + private String container; + private String toolName; + + @Override public boolean start() throws Exception { + EnvVars envReduced = new EnvVars(env); + EnvVars envHost = computer.getEnvironment(); + envReduced.entrySet().removeAll(envHost.entrySet()); + LOGGER.log(Level.FINE, "reduced environment: {0}", envReduced); + workspace.mkdirs(); // otherwise it may be owned by root when created for -v + String ws = workspace.getRemote(); + toolName = step.toolName; + DockerClient dockerClient = new DockerClient(launcher, node, toolName); + + // Add a warning if the docker version is less than 1.3 + VersionNumber dockerVersion = dockerClient.version(); + if (dockerVersion != null) { + if (dockerVersion.isOlderThan(new VersionNumber("1.3"))) { + throw new AbortException("The docker version is less than v1.3. Workflow functions requiring 'docker exec' will not work e.g. 'docker.inside'."); + } + } else { + listener.error("Failed to parse docker version. Please note there is a minimum docker version requirement of v1.3."); + } + + container = dockerClient.run(env, step.image, step.args, ws, Collections.singletonMap(ws, ws), envReduced, dockerClient.whoAmI(), /* expected to hang until killed */ "cat"); + DockerFingerprints.addRunFacet(dockerClient.getContainerRecord(env, container), run); + ImageAction.add(step.image, run); + getContext().newBodyInvoker(). + withContext(BodyInvoker.mergeLauncherDecorators(getContext().get(LauncherDecorator.class), new Decorator(container, envHost))). + withCallback(new Callback(container, toolName)). + start(); + return false; + } + + @Override public void stop(Throwable cause) throws Exception { + if (container != null) { + destroy(container, launcher, getContext().get(Node.class), env, toolName); + } + } + + } + + private static class Decorator extends LauncherDecorator implements Serializable { + + private static final long serialVersionUID = 1; + private final String container; + private final String[] envHost; + + Decorator(String container, EnvVars envHost) { + this.container = container; + this.envHost = Util.mapToEnv(envHost); + } + + @Override public Launcher decorate(Launcher launcher, Node node) { + return new Launcher.DecoratedLauncher(launcher) { + @Override public Proc launch(Launcher.ProcStarter starter) throws IOException { + List prefix = new ArrayList(Arrays.asList("docker", "exec", container, "env")); + Set envReduced = new TreeSet(Arrays.asList(starter.envs())); + envReduced.removeAll(Arrays.asList(envHost)); + starter.envs(new String[0]); + prefix.addAll(envReduced); + // Adapted from decorateByPrefix: + starter.cmds().addAll(0, prefix); + if (starter.masks() != null) { + boolean[] masks = new boolean[starter.masks().length + prefix.size()]; + System.arraycopy(starter.masks(), 0, masks, prefix.size(), starter.masks().length); + starter.masks(masks); + } + return super.launch(starter); + } + @Override public void kill(Map modelEnvVars) throws IOException, InterruptedException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + if (getInner().launch().cmds("docker", "exec", container, "ps", "-A", "-o", "pid,command", "e").stdout(baos).quiet(true).join() != 0) { + throw new IOException("failed to run ps"); + } + List pids = new ArrayList(); + LINE: for (String line : baos.toString().split("\n")) { + for (Map.Entry entry : modelEnvVars.entrySet()) { + // TODO this is imprecise: false positive when argv happens to match KEY=value even if environment does not. Cf. trick in BourneShellScript. + if (!line.contains(entry.getKey() + "=" + entry.getValue())) { + continue LINE; + } + } + line = line.trim(); + int spc = line.indexOf(' '); + if (spc == -1) { + continue; + } + pids.add(line.substring(0, spc)); + } + LOGGER.log(Level.FINE, "killing {0}", pids); + if (!pids.isEmpty()) { + List cmds = new ArrayList(Arrays.asList("docker", "exec", container, "kill")); + cmds.addAll(pids); + if (getInner().launch().cmds(cmds).quiet(true).join() != 0) { + throw new IOException("failed to run kill"); + } + } + } + }; + } + + } + + private static class Callback extends BodyExecutionCallback { + + private static final long serialVersionUID = 1; + private final String container; + private final String toolName; + + Callback(String container, String toolName) { + this.container = container; + this.toolName = toolName; + } + + private void stopContainer(StepContext context) { + Launcher launcher; + TaskListener listener; + try { + launcher = context.get(Launcher.class); + listener = context.get(TaskListener.class); + } catch (Exception x2) { + LOGGER.log(Level.WARNING, null, x2); + return; + } + try { + EnvVars launcherEnv = context.get(EnvVars.class); + Node node = context.get(Node.class); + destroy(container, launcher, node, launcherEnv, toolName); + } catch (Exception x) { + x.printStackTrace(listener.error("Could not kill container " + container)); + } + } + + @Override public void onSuccess(StepContext context, Object result) { + stopContainer(context); + context.onSuccess(result); + } + + @Override public void onFailure(StepContext context, Throwable t) { + stopContainer(context); + context.onFailure(t); + } + + } + + @Extension public static class DescriptorImpl extends AbstractStepDescriptorImpl { + + public DescriptorImpl() { + super(Execution.class); + } + + @Override public String getFunctionName() { + return "withDockerContainer"; + } + + @Override public String getDisplayName() { + return "Run build steps inside a Docker container"; + } + + @Override public boolean takesImplicitBlockArgument() { + return true; + } + + @Override public boolean isAdvanced() { + return true; + } + + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java new file mode 100644 index 000000000..01f883e0d --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java @@ -0,0 +1,271 @@ +/* + * The MIT License + * + * Copyright (c) 2015, CloudBees, 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. + */ +package org.jenkinsci.plugins.docker.workflow.client; + +import hudson.EnvVars; +import hudson.FilePath; +import hudson.Launcher; +import hudson.model.Node; +import hudson.util.ArgumentListBuilder; +import hudson.util.VersionNumber; +import org.jenkinsci.plugins.docker.commons.fingerprint.ContainerRecord; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Date; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.jenkinsci.plugins.docker.commons.tools.DockerTool; + +/** + * Simple docker client for workflow. + * + * @author tom.fennelly@gmail.com + */ +public class DockerClient { + + private static final Logger LOGGER = Logger.getLogger(DockerClient.class.getName()); + + // e.g. 2015-04-09T13:40:21.981801679Z + public static final String DOCKER_DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"; + + private Launcher launcher; + private final @CheckForNull Node node; + private final @CheckForNull String toolName; + + public DockerClient(@Nonnull Launcher launcher, @CheckForNull Node node, @CheckForNull String toolName) { + this.launcher = launcher; + this.node = node; + this.toolName = toolName; + } + + /** + * Run a docker image. + * + * @param launchEnv Docker client launch environment. + * @param image The image name. + * @param args Any additional arguments for the {@code docker run} command. + * @param workdir The working directory in the container, or {@code null} for default. + * @param volumes Volumes to be bound. Supply an empty list if no volumes are to be bound. + * @param containerEnv Environment variables to set in container. + * @param user The uid:gid to execute the container command as. Use {@link #whoAmI()}. + * @param command The command to execute in the image container being run. + * @return The container ID. + */ + public String run(@Nonnull EnvVars launchEnv, @Nonnull String image, @CheckForNull String args, @CheckForNull String workdir, @Nonnull Map volumes, @Nonnull EnvVars containerEnv, @Nonnull String user, @CheckForNull String... command) throws IOException, InterruptedException { + ArgumentListBuilder argb = new ArgumentListBuilder(); + + argb.add("run", "-t", "-d", "-u", user); + if (argb != null) { + argb.addTokenized(args); + } + if (workdir != null) { + argb.add("-w", workdir); + } + for (Map.Entry volume : volumes.entrySet()) { + argb.add("-v", volume.getKey() + ":" + volume.getValue() + ":rw"); + } + for (Map.Entry variable : containerEnv.entrySet()) { + argb.add("-e"); + argb.addMasked(variable.getKey()+"="+variable.getValue()); + } + if (command != null) { + argb.add(image).add(command); + } + + LaunchResult result = launch(launchEnv, false, null, argb); + if (result.getStatus() == 0) { + return result.getOut(); + } else { + throw new IOException(String.format("Failed to run image '%s'. Error: %s", image, result.getErr())); + } + } + + /** + * Stop a container. + * + *

+ * Also removes ({@link #rm(EnvVars, String)}) the container. + * + * @param launchEnv Docker client launch environment. + * @param containerId The container ID. + */ + public void stop(@Nonnull EnvVars launchEnv, @Nonnull String containerId) throws IOException, InterruptedException { + LaunchResult result = launch(launchEnv, false, "stop", containerId); + if (result.getStatus() != 0) { + throw new IOException(String.format("Failed to kill container '%s'.", containerId)); + } + rm(launchEnv, containerId); + } + + /** + * Remove a container. + * + * @param launchEnv Docker client launch environment. + * @param containerId The container ID. + */ + public void rm(@Nonnull EnvVars launchEnv, @Nonnull String containerId) throws IOException, InterruptedException { + LaunchResult result; + result = launch(launchEnv, false, "rm", "-f", containerId); + if (result.getStatus() != 0) { + throw new IOException(String.format("Failed to rm container '%s'.", containerId)); + } + } + + /** + * Inspect a docker image/container. + * @param launchEnv Docker client launch environment. + * @param objectId The image/container ID. + * @param fieldPath The data path of the data required e.g. {@code .NetworkSettings.IPAddress}. + * @return The inspected field value. + */ + public @CheckForNull String inspect(@Nonnull EnvVars launchEnv, @Nonnull String objectId, @Nonnull String fieldPath) throws IOException, InterruptedException { + LaunchResult result = launch(launchEnv, true, "inspect", "-f", String.format("{{%s}}", fieldPath), objectId); + if (result.getStatus() == 0) { + return result.getOut(); + } else { + return null; + } + } + + private Date getCreatedDate(@Nonnull EnvVars launchEnv, @Nonnull String objectId) throws IOException, InterruptedException { + String createdString = inspect(launchEnv, objectId, ".Created"); + if (createdString == null) { + return null; + } + try { + // TODO Currently truncating. Find out how to specify last part for parsing (TZ etc) + return new SimpleDateFormat(DOCKER_DATE_TIME_FORMAT).parse(createdString.substring(0, DOCKER_DATE_TIME_FORMAT.length() - 2)); + } catch (ParseException e) { + throw new IOException(String.format("Error parsing created date '%s' for object '%s'.", createdString, objectId), e); + } + } + + /** + * Get the docker version. + * + * @return The {@link VersionNumber} instance if the version string matches the expected format, + * otherwise {@code null}. + */ + public @CheckForNull VersionNumber version() throws IOException, InterruptedException { + LaunchResult result = launch(new EnvVars(), true, "-v"); + if (result.getStatus() == 0) { + return parseVersionNumber(result.getOut()); + } else { + return null; + } + } + + private static final Pattern pattern = Pattern.compile("^(\\D+)(\\d+)\\.(\\d+)\\.(\\d+)(.*)"); + /** + * Parse a Docker version string (e.g. "Docker version 1.5.0, build a8a31ef"). + * @param versionString The version string to parse. + * @return The {@link VersionNumber} instance if the version string matched the + * expected format, otherwise {@code null}. + */ + protected static VersionNumber parseVersionNumber(@Nonnull String versionString) { + Matcher matcher = pattern.matcher(versionString.trim()); + if (matcher.matches()) { + String major = matcher.group(2); + String minor = matcher.group(3); + String maint = matcher.group(4); + return new VersionNumber(String.format("%s.%s.%s", major, minor, maint)); + } else { + return null; + } + } + + private LaunchResult launch(@Nonnull EnvVars launchEnv, boolean quiet, @Nonnull String... args) throws IOException, InterruptedException { + return launch(launchEnv, quiet, null, args); + } + private LaunchResult launch(@Nonnull EnvVars launchEnv, boolean quiet, FilePath pwd, @Nonnull String... args) throws IOException, InterruptedException { + return launch(launchEnv, quiet, pwd, new ArgumentListBuilder(args)); + } + private LaunchResult launch(@CheckForNull @Nonnull EnvVars launchEnv, boolean quiet, FilePath pwd, @Nonnull ArgumentListBuilder args) throws IOException, InterruptedException { + // Prepend the docker command + args.prepend(DockerTool.getExecutable(toolName, node, launcher.getListener(), launchEnv)); + + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.log(Level.FINE, "Executing docker command {0}", args.toString()); + } + + Launcher.ProcStarter procStarter = launcher.launch(); + + if (pwd != null) { + procStarter.pwd(pwd); + } + + LaunchResult result = new LaunchResult(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + try { + result.setStatus(procStarter.quiet(quiet).cmds(args).envs(launchEnv).stdout(out).stderr(err).join()); + return result; + } finally { + try { + result.setOut(out.toString()); + out.close(); + } finally { + result.setErr(err.toString()); + err.close(); + } + } + } + + /** + * Who is executing this {@link DockerClient} instance. + * + * @return a {@link String} containing the uid:gid. + */ + public String whoAmI() throws IOException, InterruptedException { + ByteArrayOutputStream userId = new ByteArrayOutputStream(); + launcher.launch().cmds("id", "-u").quiet(true).stdout(userId).join(); + + ByteArrayOutputStream groupId = new ByteArrayOutputStream(); + launcher.launch().cmds("id", "-g").quiet(true).stdout(groupId).join(); + + return String.format("%s:%s", userId.toString().trim(), groupId.toString().trim()); + + } + + public ContainerRecord getContainerRecord(@Nonnull EnvVars launchEnv, String containerId) throws IOException, InterruptedException { + String host = inspect(launchEnv, containerId, ".Config.Hostname"); + String containerName = inspect(launchEnv, containerId, ".Name"); + Date created = getCreatedDate(launchEnv, containerId); + String image = inspect(launchEnv, containerId, ".Image"); + + // TODO get tags and add for ContainerRecord + return new ContainerRecord(host, containerId, image, containerName, + (created != null ? created.getTime() : 0L), + Collections.emptyMap()); + } +} diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/client/LaunchResult.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/client/LaunchResult.java new file mode 100644 index 000000000..b26f06072 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/client/LaunchResult.java @@ -0,0 +1,70 @@ +/* + * The MIT License + * + * Copyright (c) 2015, CloudBees, 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. + */ +package org.jenkinsci.plugins.docker.workflow.client; + +/** + * Launch result. + * + * @author tom.fennelly@gmail.com + */ +public class LaunchResult { + + private int status; + private String out; + private String err; + + LaunchResult() { + } + + public int getStatus() { + return status; + } + + LaunchResult setStatus(int status) { + this.status = status; + return this; + } + + public String getOut() { + return out; + } + + LaunchResult setOut(String out) { + if (out != null) { + this.out = out.trim(); + } + return this; + } + + public String getErr() { + return err; + } + + LaunchResult setErr(String err) { + if (err != null) { + this.err = err.trim(); + } + return this; + } +} diff --git a/src/main/resources/index.jelly b/src/main/resources/index.jelly new file mode 100644 index 000000000..fe93b8e1b --- /dev/null +++ b/src/main/resources/index.jelly @@ -0,0 +1,5 @@ + + +

+ Build and use Docker containers from workflows. +
diff --git a/src/main/resources/org/jenkinsci/plugins/docker/workflow/Docker.groovy b/src/main/resources/org/jenkinsci/plugins/docker/workflow/Docker.groovy new file mode 100644 index 000000000..722baa4c9 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/docker/workflow/Docker.groovy @@ -0,0 +1,179 @@ +/* + * The MIT License + * + * Copyright (c) 2015, CloudBees, 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. + */ +package org.jenkinsci.plugins.docker.workflow + +class Docker implements Serializable { + + private org.jenkinsci.plugins.workflow.cps.CpsScript script + + public Docker(org.jenkinsci.plugins.workflow.cps.CpsScript script) { + this.script = script + } + + public V withRegistry(String url, String credentialsId = null, Closure body) { + node { + script.withEnv(["DOCKER_REGISTRY_URL=${url}"]) { + script.withDockerRegistry(registry: [url: url, credentialsId: credentialsId]) { + body() + } + } + } + } + + public V withServer(String uri, String credentialsId = null, Closure body) { + node { + script.withDockerServer(server: [uri: uri, credentialsId: credentialsId]) { + body() + } + } + } + + public V withTool(String toolName, Closure body) { + node { + script.withEnv(["PATH=${script.tool name: toolName, type: 'org.jenkinsci.plugins.docker.commons.tools.DockerTool'}/bin:${script.env.PATH}", "DOCKER_TOOL_NAME=${toolName}"]) { + body() + } + } + } + + private V node(Closure body) { + if (script.env.HOME != null) { // http://unix.stackexchange.com/a/123859/26736 + // Already inside a node block. + body() + } else { + script.node { + body() + } + } + } + + public Image image(String id) { + new Image(this, id) + } + + public Image build(String image, String dir = '.') { + node { + script.sh "docker build -t ${image} ${dir}" + script.dockerFingerprintFrom dockerfile: "${dir}/Dockerfile", image: image, toolName: script.env.DOCKER_TOOL_NAME + this.image(image) + } + } + + public static class Image implements Serializable { + + private final Docker docker; + public final String id; + private ImageNameTokens parsedId; + + private Image(Docker docker, String id) { + this.docker = docker + this.id = id + this.parsedId = new ImageNameTokens(id) + } + + private String toQualifiedImageName(String imageName) { + return new org.jenkinsci.plugins.docker.commons.credentials.DockerRegistryEndpoint(docker.script.env.DOCKER_REGISTRY_URL, null).imageName(imageName) + } + + public String imageName() { + return toQualifiedImageName(id) + } + + public V inside(String args = '', Closure body) { + docker.node { + try { + docker.script.sh "docker inspect -f . ${id}" + // OK, already pulled + } catch (hudson.AbortException e) { + // withDockerContainer requires the image to be available locally, since its start phase is not a durable task. + pull() + } + docker.script.withDockerContainer(image: id, args: args, toolName: docker.script.env.DOCKER_TOOL_NAME) { + body() + } + } + } + + public void pull() { + docker.node { + docker.script.sh "docker pull ${imageName()}" + } + } + + public Container run(String args = '') { + docker.node { + docker.script.sh "docker run -d${args != '' ? ' ' + args : ''} ${id} > .container" + def container = docker.script.readFile('.container').trim() + docker.script.dockerFingerprintRun containerId: container, toolName: docker.script.env.DOCKER_TOOL_NAME + new Container(docker, container) + } + } + + public V withRun(String args = '', Closure body) { + docker.node { + Container c = run(args) + try { + body.call(c) + } finally { + c.stop() + } + } + } + + public void tag(String tagName = parsedId.tag, boolean force = true) { + docker.node { + def taggedImageName = toQualifiedImageName(parsedId.userAndRepo + ':' + tagName) + docker.script.sh "docker tag --force=${force} ${id} ${taggedImageName}" + return taggedImageName; + } + } + + public void push(String tagName = parsedId.tag, boolean force = true) { + docker.node { + // The image may have already been tagged, so the tagging may be a no-op. + // That's ok since tagging is cheap. + def taggedImageName = tag(tagName, force) + docker.script.sh "docker push ${taggedImageName}" + } + } + + } + + public static class Container implements Serializable { + + private final Docker docker; + public final String id; + + private Container(Docker docker, String id) { + this.docker = docker + this.id = id + } + + public void stop() { + docker.script.sh "docker stop ${id} && docker rm -f ${id}" + } + + } + +} diff --git a/src/main/resources/org/jenkinsci/plugins/docker/workflow/DockerDSL/help.jelly b/src/main/resources/org/jenkinsci/plugins/docker/workflow/DockerDSL/help.jelly new file mode 100644 index 000000000..e5e6b2556 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/docker/workflow/DockerDSL/help.jelly @@ -0,0 +1,120 @@ + + + +

+ The docker variable offers convenient access to Docker-related functions from a Workflow script. +

+

+ Methods needing a slave will implicitly run a node {…} block if you have not wrapped them in one. + It is a good idea to enclose a block of steps which should all run on the same node in such a block yourself. + (If using a Swarm server, or any other specific Docker server, this probably does not matter, but if you are using the default server on localhost it likely will.) +

+

+ Some methods return instances of auxiliary classes which serve as holders for an ID and which have their own methods and properties. + Methods taking a body return any value returned by the body itself. + Some method parameters are optional and are enclosed with []. + Reference: +

+
+
withRegistry(url[, credentialsId]) {…}
+
+

+ Specifies a registry URL such as https://docker.mycorp.com/, plus an optional credentials ID to connect to it. +

+
+
withServer(uri[, credentialsId]) {…}
+
+

+ Specifies a server URI such as tcp://swarm.mycorp.com:2376, plus an optional credentials ID to connect to it. +

+
+
withTool(toolName) {…}
+
+

+ Specifies the name of a Docker installation to use, if any are defined in Jenkins global configuration. + If unspecified, docker is assumed to be in the $PATH of the slave agent. +

+
+
image(id)
+
+

+ Creates an Image object with a specified name or ID. See below. +

+
+
build(image[, dir])
+
+

+ Runs docker build to create and tag the specified image from a Dockerfile in the current directory (or dir). + Returns the resulting Image object. + Records a FROM fingerprint in the build. +

+
+
Image.id
+
+

+ The image name with optional tag (mycorp/myapp, mycorp/myapp:latest) or ID (hexadecimal hash). +

+
+
Image.run([args])
+
+

+ Uses docker run to run the image, and returns a Container which you could stop later. + Additional args may be added, such as '-p 8080:8080 --memory-swap=-1'. + Records a run fingerprint in the build. +

+
+
Image.withRun[(args)] {…}
+
+

+ Like run but stops the container as soon as its body exits, so you do not need a try-finally block. +

+
+
Image.inside[(args)] {…}
+
+

+ Like withRun this starts a container for the duration of the body, but all external commands (sh) launched by the body run inside the container rather than on the host. + These commands run in the same working directory (normally a slave workspace), which means that the Docker server must be on localhost. +

+
+
Image.tag([tagname[, force]])
+
+

+ Runs docker tag to record a tag of this image (defaulting to the tag it already has). + By default will rewrite an existing tag; pass false for the second argument to fail instead. +

+
+
Image.push([tagname[, force]])
+
+

+ Pushes an image to the registry after tagging it as with the tag method. + For example, you can use image.push 'latest' to publish it as the latest version in its repository. +

+
+
Image.pull()
+
+

+ Runs docker pull. + Not necessary before run, withRun, or inside. +

+
+
Image.imageName()
+
+

+ The id prefixed as needed with registry information, such as docker.mycorp.com/mycorp/myapp. + May be used if running your own Docker commands using sh. +

+
+
Container.id
+
+

+ Hexadecimal ID of a running container. +

+
+
Container.stop
+
+

+ Runs docker stop and docker rm to shut down a container and remove its storage. +

+
+
+
diff --git a/src/main/resources/org/jenkinsci/plugins/docker/workflow/FromFingerprintStep/config.jelly b/src/main/resources/org/jenkinsci/plugins/docker/workflow/FromFingerprintStep/config.jelly new file mode 100644 index 000000000..d886c65d5 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/docker/workflow/FromFingerprintStep/config.jelly @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/docker/workflow/FromFingerprintStep/help-dockerfile.html b/src/main/resources/org/jenkinsci/plugins/docker/workflow/FromFingerprintStep/help-dockerfile.html new file mode 100644 index 000000000..6fcb21269 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/docker/workflow/FromFingerprintStep/help-dockerfile.html @@ -0,0 +1,3 @@ +
+ Workspace-relative path of a Dockerfile which will be parsed for its FROM instruction. +
diff --git a/src/main/resources/org/jenkinsci/plugins/docker/workflow/FromFingerprintStep/help-image.html b/src/main/resources/org/jenkinsci/plugins/docker/workflow/FromFingerprintStep/help-image.html new file mode 100644 index 000000000..4b3882a02 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/docker/workflow/FromFingerprintStep/help-image.html @@ -0,0 +1,3 @@ +
+ ID or tag of the image which was just built, like --tag of docker build. +
diff --git a/src/main/resources/org/jenkinsci/plugins/docker/workflow/FromFingerprintStep/help.html b/src/main/resources/org/jenkinsci/plugins/docker/workflow/FromFingerprintStep/help.html new file mode 100644 index 000000000..66880b32a --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/docker/workflow/FromFingerprintStep/help.html @@ -0,0 +1,4 @@ +
+ Normally used implicitly by method calls on the docker global variable. + Records the fact that a Docker image was built from another. +
diff --git a/src/main/resources/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStep/config.jelly b/src/main/resources/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStep/config.jelly new file mode 100644 index 000000000..15f6a2ce9 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStep/config.jelly @@ -0,0 +1,28 @@ + + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStep/help.html b/src/main/resources/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStep/help.html new file mode 100644 index 000000000..3f96972a4 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStep/help.html @@ -0,0 +1,4 @@ +
+ Normally used implicitly by method calls on the docker global variable. + Sets up connection details for a Docker registry. +
diff --git a/src/main/resources/org/jenkinsci/plugins/docker/workflow/RunFingerprintStep/config.jelly b/src/main/resources/org/jenkinsci/plugins/docker/workflow/RunFingerprintStep/config.jelly new file mode 100644 index 000000000..7be20ef9b --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/docker/workflow/RunFingerprintStep/config.jelly @@ -0,0 +1,31 @@ + + + + + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/docker/workflow/RunFingerprintStep/help.html b/src/main/resources/org/jenkinsci/plugins/docker/workflow/RunFingerprintStep/help.html new file mode 100644 index 000000000..22c429faf --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/docker/workflow/RunFingerprintStep/help.html @@ -0,0 +1,4 @@ +
+ Normally used implicitly by method calls on the docker global variable. + Records the fact that a Docker image was used by this build. +
diff --git a/src/main/resources/org/jenkinsci/plugins/docker/workflow/ServerEndpointStep/config.jelly b/src/main/resources/org/jenkinsci/plugins/docker/workflow/ServerEndpointStep/config.jelly new file mode 100644 index 000000000..d53845399 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/docker/workflow/ServerEndpointStep/config.jelly @@ -0,0 +1,28 @@ + + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/docker/workflow/ServerEndpointStep/help.html b/src/main/resources/org/jenkinsci/plugins/docker/workflow/ServerEndpointStep/help.html new file mode 100644 index 000000000..87ef21bf5 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/docker/workflow/ServerEndpointStep/help.html @@ -0,0 +1,4 @@ +
+ Normally used implicitly by method calls on the docker global variable. + Sets up connection details for a Docker server. +
diff --git a/src/main/resources/org/jenkinsci/plugins/docker/workflow/WithContainerStep/config.jelly b/src/main/resources/org/jenkinsci/plugins/docker/workflow/WithContainerStep/config.jelly new file mode 100644 index 000000000..0c703db1b --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/docker/workflow/WithContainerStep/config.jelly @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/docker/workflow/WithContainerStep/help-args.html b/src/main/resources/org/jenkinsci/plugins/docker/workflow/WithContainerStep/help-args.html new file mode 100644 index 000000000..0f46f5561 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/docker/workflow/WithContainerStep/help-args.html @@ -0,0 +1,3 @@ +
+ Any other arguments to pass to docker run. +
diff --git a/src/main/resources/org/jenkinsci/plugins/docker/workflow/WithContainerStep/help.html b/src/main/resources/org/jenkinsci/plugins/docker/workflow/WithContainerStep/help.html new file mode 100644 index 000000000..af831a63c --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/docker/workflow/WithContainerStep/help.html @@ -0,0 +1,7 @@ +
+ Normally used implicitly by method calls on the docker global variable. + Takes an image ID or symbolic name which must already have been pulled locally + and starts a container based on that image. + Runs all nested sh steps inside that container. + The workspace is mounted read-write into the container. +
diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerDSLTest.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerDSLTest.java new file mode 100644 index 000000000..ec4c19b0b --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerDSLTest.java @@ -0,0 +1,338 @@ +/* + * The MIT License + * + * Copyright (c) 2015, CloudBees, 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. + */ +package org.jenkinsci.plugins.docker.workflow; + +import static org.jenkinsci.plugins.docker.workflow.DockerTestUtil.assumeDocker; + +import org.jenkinsci.plugins.docker.workflow.client.DockerClient; +import hudson.EnvVars; +import hudson.Launcher; +import hudson.model.Fingerprint; +import hudson.tools.ToolProperty; +import hudson.util.StreamTaskListener; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.TreeSet; +import org.apache.commons.io.FileUtils; +import org.jenkinsci.plugins.docker.commons.DockerImageExtractor; +import org.jenkinsci.plugins.docker.commons.tools.DockerTool; +import org.jenkinsci.plugins.docker.commons.fingerprint.DockerAncestorFingerprintFacet; +import org.jenkinsci.plugins.docker.commons.fingerprint.DockerDescendantFingerprintFacet; +import org.jenkinsci.plugins.docker.commons.fingerprint.DockerFingerprints; +import org.jenkinsci.plugins.docker.commons.fingerprint.DockerRunFingerprintFacet; +import org.jenkinsci.plugins.workflow.BuildWatcher; +import org.jenkinsci.plugins.workflow.JenkinsRuleExt; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep; +import static org.junit.Assert.*; +import static org.junit.Assume.*; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runners.model.Statement; +import org.jvnet.hudson.test.RestartableJenkinsRule; + +public class DockerDSLTest { + + @ClassRule public static BuildWatcher buildWatcher = new BuildWatcher(); + @Rule public RestartableJenkinsRule story = new RestartableJenkinsRule(); + + @Test public void firstDoNoHarm() { + story.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + assumeDocker(); + WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition("semaphore 'wait'", true)); + WorkflowRun b = p.scheduleBuild2(0).waitForStart(); + SemaphoreStep.waitForStart("wait/1", b); + } + }); + story.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + WorkflowJob p = story.j.jenkins.getItemByFullName("p", WorkflowJob.class); + WorkflowRun b = p.getLastBuild(); + assertEquals(Collections.emptySet(), grep(b.getRootDir(), "org.jenkinsci.plugins.docker.workflow.Docker")); + SemaphoreStep.success("wait/1", null); + story.j.assertBuildStatusSuccess(JenkinsRuleExt.waitForCompletion(b)); + } + }); + } + // TODO copied from BindingStepTest, should be made into a utility in Jenkins test harness perhaps (or JenkinsRuleExt as a first step) + private static Set grep(File dir, String text) throws IOException { + Set matches = new TreeSet(); + grep(dir, text, "", matches); + return matches; + } + private static void grep(File dir, String text, String prefix, Set matches) throws IOException { + File[] kids = dir.listFiles(); + if (kids == null) { + return; + } + for (File kid : kids) { + String qualifiedName = prefix + kid.getName(); + if (kid.isDirectory()) { + grep(kid, text, qualifiedName + "/", matches); + } else if (kid.isFile() && FileUtils.readFileToString(kid).contains(text)) { + matches.add(qualifiedName); + } + } + } + + + @Test public void inside() { + story.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + assumeDocker(); + WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "def r = docker.image('httpd:2.4.12').inside {\n" + + " semaphore 'wait'\n" + + " sh 'cat /usr/local/apache2/conf/extra/httpd-userdir.conf'\n" + + " 42\n" + + "}; echo \"the answer is ${r}\"", true)); + WorkflowRun b = p.scheduleBuild2(0).waitForStart(); + SemaphoreStep.waitForStart("wait/1", b); + } + }); + story.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + SemaphoreStep.success("wait/1", null); + WorkflowJob p = story.j.jenkins.getItemByFullName("p", WorkflowJob.class); + WorkflowRun b = p.getLastBuild(); + story.j.assertLogContains("Require method GET POST OPTIONS", story.j.assertBuildStatusSuccess(JenkinsRuleExt.waitForCompletion(b))); + story.j.assertLogContains("the answer is 42", b); + DockerClient client = new DockerClient(new Launcher.LocalLauncher(StreamTaskListener.NULL), null, null); + String httpdIID = client.inspect(new EnvVars(), "httpd:2.4.12", ".Id"); + Fingerprint f = DockerFingerprints.of(httpdIID); + assertNotNull(f); + DockerRunFingerprintFacet facet = f.getFacet(DockerRunFingerprintFacet.class); + assertNotNull(facet); + assertEquals(1, facet.records.size()); + assertNotNull(facet.records.get(0).getContainerName()); + assertEquals(Fingerprint.RangeSet.fromString("1", false), facet.getRangeSet(p)); + assertEquals(Collections.singleton("httpd"), DockerImageExtractor.getDockerImagesUsedByJobFromAll(p)); + } + }); + } + + @Test public void endpoints() { + story.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + // do withRegistry before withServer until JENKINS-28317 fix in Workflow 1.7: + "docker.withRegistry('https://docker.my.com/') {\n" + + " docker.withServer('tcp://host:1234') {\n" + + " semaphore 'wait'\n" + + " sh 'echo would be connecting to $DOCKER_HOST'\n" + + " echo \"image name is ${docker.image('whatever').imageName()}\"\n" + + " }\n" + + "}", true)); + WorkflowRun b = p.scheduleBuild2(0).waitForStart(); + SemaphoreStep.waitForStart("wait/1", b); + } + }); + story.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + SemaphoreStep.success("wait/1", null); + WorkflowJob p = story.j.jenkins.getItemByFullName("p", WorkflowJob.class); + WorkflowRun b = story.j.assertBuildStatusSuccess(JenkinsRuleExt.waitForCompletion(p.getLastBuild())); + story.j.assertLogContains("would be connecting to tcp://host:1234", b); + story.j.assertLogContains("image name is docker.my.com/whatever", b); + } + }); + } + + @Test public void runArgs() { + story.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + assumeDocker(); + WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node {\n" + + " def img = docker.image('httpd:2.4.12')\n" + + " img.run().stop()\n" + + " img.run('--memory-swap=-1').stop()\n" + + " img.withRun {}\n" + + " img.withRun('--memory-swap=-1') {}\n" + + " img.inside {}\n" + + " img.inside('--memory-swap=-1') {}\n" + + "}", true)); + story.j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + } + }); + } + + @Test public void withRun() { + story.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + assumeDocker(); + WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "def r = docker.image('httpd:2.4.12').withRun {c ->\n" + + " semaphore 'wait'\n" + + " sh \"docker exec ${c.id} cat /usr/local/apache2/conf/extra/httpd-userdir.conf\"\n" + + " 42\n" + + "}; echo \"the answer is ${r}\"", true)); + WorkflowRun b = p.scheduleBuild2(0).waitForStart(); + SemaphoreStep.waitForStart("wait/1", b); + } + }); + story.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + SemaphoreStep.success("wait/1", null); + WorkflowJob p = story.j.jenkins.getItemByFullName("p", WorkflowJob.class); + WorkflowRun b = p.getLastBuild(); + story.j.assertLogContains("Require method GET POST OPTIONS", story.j.assertBuildStatusSuccess(JenkinsRuleExt.waitForCompletion(b))); + story.j.assertLogContains("the answer is 42", b); + DockerClient client = new DockerClient(new Launcher.LocalLauncher(StreamTaskListener.NULL), null, null); + String httpdIID = client.inspect(new EnvVars(), "httpd:2.4.12", ".Id"); + Fingerprint f = DockerFingerprints.of(httpdIID); + assertNotNull(f); + DockerRunFingerprintFacet facet = f.getFacet(DockerRunFingerprintFacet.class); + assertNotNull(facet); + assertEquals(1, facet.records.size()); + assertNotNull(facet.records.get(0).getContainerName()); + assertEquals(Fingerprint.RangeSet.fromString("1", false), facet.getRangeSet(p)); + assertEquals(Collections.singleton("httpd"), DockerImageExtractor.getDockerImagesUsedByJobFromAll(p)); + } + }); + } + + @Test public void build() { + story.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + assumeDocker(); + WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p"); + String ancestorImageId = "91c95931e552b11604fea91c2f537284149ec32fff0f700a4769cfd31d7696ae"; + p.setDefinition(new CpsFlowDefinition( + "node {\n" + + " writeFile file: 'stuff1', text: 'hello'\n" + + " writeFile file: 'stuff2', text: 'world'\n" + + " writeFile file: 'stuff3', text: env.BUILD_NUMBER\n" + + " sh 'touch -t 201501010000 stuff*'\n" + // image hash includes timestamps! + " writeFile file: 'Dockerfile', text: '# This is a test.\\n\\nFROM hello-world@" + ancestorImageId + "\\nCOPY stuff1 /\\nCOPY stuff2 /\\nCOPY stuff3 /\\n'\n" + + " def built = docker.build 'hello-world-stuff'\n" + + " echo \"built ${built.id}\"\n" + + "}", true)); + WorkflowRun b = story.j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + String descendantImageId1 = "c04cd8a9a37440562c655c8076dc47a41ae7855096e73c6e8aa5f01f2ed52b85"; + story.j.assertLogContains("built hello-world-stuff", b); + story.j.assertLogContains(descendantImageId1.substring(0, 12), b); + Fingerprint f = DockerFingerprints.of(ancestorImageId); + assertNotNull(f); + DockerDescendantFingerprintFacet descendantFacet = f.getFacet(DockerDescendantFingerprintFacet.class); + assertNotNull(descendantFacet); + assertEquals(Fingerprint.RangeSet.fromString("1", false), descendantFacet.getRangeSet(p)); + assertEquals(ancestorImageId, descendantFacet.getImageId()); + assertEquals(Collections.singleton(descendantImageId1), descendantFacet.getDescendantImageIds()); + f = DockerFingerprints.of(descendantImageId1); + assertNotNull(f); + DockerAncestorFingerprintFacet ancestorFacet = f.getFacet(DockerAncestorFingerprintFacet.class); + assertNotNull(ancestorFacet); + assertEquals(Fingerprint.RangeSet.fromString("1", false), ancestorFacet.getRangeSet(p)); + assertEquals(Collections.singleton(ancestorImageId), ancestorFacet.getAncestorImageIds()); + assertEquals(descendantImageId1, ancestorFacet.getImageId()); + b = story.j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + String descendantImageId2 = "0703ebc9f6a713f56191ce4db96338f4572de53479bc32efd60717f789d91089"; + story.j.assertLogContains("built hello-world-stuff", b); + story.j.assertLogContains(descendantImageId2.substring(0, 12), b); + f = DockerFingerprints.of(ancestorImageId); + assertNotNull(f); + descendantFacet = f.getFacet(DockerDescendantFingerprintFacet.class); + assertNotNull(descendantFacet); + assertEquals(Fingerprint.RangeSet.fromString("1-2", false), descendantFacet.getRangeSet(p)); + assertEquals(ancestorImageId, descendantFacet.getImageId()); + assertEquals(new HashSet(Arrays.asList(descendantImageId1, descendantImageId2)), descendantFacet.getDescendantImageIds()); + f = DockerFingerprints.of(descendantImageId1); + assertNotNull(f); + ancestorFacet = f.getFacet(DockerAncestorFingerprintFacet.class); + assertNotNull(ancestorFacet); + assertEquals(Fingerprint.RangeSet.fromString("1", false), ancestorFacet.getRangeSet(p)); + assertEquals(Collections.singleton(ancestorImageId), ancestorFacet.getAncestorImageIds()); + assertEquals(descendantImageId1, ancestorFacet.getImageId()); + f = DockerFingerprints.of(descendantImageId2); + assertNotNull(f); + ancestorFacet = f.getFacet(DockerAncestorFingerprintFacet.class); + assertNotNull(ancestorFacet); + assertEquals(Fingerprint.RangeSet.fromString("2", false), ancestorFacet.getRangeSet(p)); + assertEquals(Collections.singleton(ancestorImageId), ancestorFacet.getAncestorImageIds()); + assertEquals(descendantImageId2, ancestorFacet.getImageId()); + assertEquals(Collections.singleton("hello-world"), DockerImageExtractor.getDockerImagesUsedByJobFromAll(p)); + } + }); + } + + @Test public void withTool() { + story.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + assumeDocker(); + assumeTrue(new File("/usr/bin/docker").canExecute()); // TODO generalize to find docker in $PATH + story.j.jenkins.getDescriptorByType(DockerTool.DescriptorImpl.class).setInstallations(new DockerTool("default", "/usr", Collections.>emptyList())); + WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "docker.withTool('default') {\n" + + " docker.image('httpd:2.4.12').withRun {}\n" + + " sh 'echo PATH=$PATH'\n" + + "}", true)); + story.j.assertLogContains("PATH=/usr/bin:", story.j.assertBuildStatusSuccess(p.scheduleBuild2(0))); + } + }); + } + + @Test public void tag() { + story.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + assumeDocker(); + WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node {\n" + + " try { sh 'docker rmi busybox:test' } catch (Exception e) {}\n" + + " def busybox = docker.image('busybox');\n" + + " busybox.pull();\n" + + " // tag it\n" + + " busybox.tag('test', false);\n" + + " // tag it again - should fail because the tag already exists and the --force flag is false\n" + + " try {\n" + + " busybox.tag('test', false);\n" + + " } catch (Exception e) {\n" + + " sh \"echo 'TAG without force failed as expected'\"\n" + + " }\n" + + " // tag it again - should work because the --force flag is true\n" + + " busybox.tag('test', true);\n" + + " sh \"echo 'TAG with force succeeded as expected'\"\n" + + "}", true)); + WorkflowRun b = story.j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + story.j.assertLogContains("TAG without force failed as expected", b); + story.j.assertLogContains("TAG with force succeeded as expected", b); + } + }); + } +} diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerTestUtil.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerTestUtil.java new file mode 100644 index 000000000..d62f43b42 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerTestUtil.java @@ -0,0 +1,53 @@ +/* + * The MIT License + * + * Copyright (c) 2015, CloudBees, 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. + */ +package org.jenkinsci.plugins.docker.workflow; + +import org.jenkinsci.plugins.docker.workflow.client.DockerClient; +import hudson.Launcher; +import hudson.util.StreamTaskListener; +import hudson.util.VersionNumber; +import org.junit.Assume; + +import java.io.IOException; + +import static org.hamcrest.Matchers.is; +import org.jenkinsci.plugins.docker.commons.tools.DockerTool; + +/** + * @author tom.fennelly@gmail.com + */ +public class DockerTestUtil { + + public static void assumeDocker() throws Exception { + Launcher.LocalLauncher localLauncher = new Launcher.LocalLauncher(StreamTaskListener.NULL); + try { + Assume.assumeThat("Docker working", localLauncher.launch().cmds(DockerTool.getExecutable(null, null, null, null), "ps").join(), is(0)); + } catch (IOException x) { + Assume.assumeNoException("have Docker installed", x); + } + DockerClient dockerClient = new DockerClient(localLauncher, null, null); + Assume.assumeFalse("Docker version not < 1.3", dockerClient.version().isOlderThan(new VersionNumber("1.3"))); + } + +} diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/ImageNameTokensTest.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/ImageNameTokensTest.java new file mode 100644 index 000000000..48deadb00 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/ImageNameTokensTest.java @@ -0,0 +1,67 @@ +/* + * The MIT License + * + * Copyright (c) 2015, CloudBees, 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. + */ +package org.jenkinsci.plugins.docker.workflow; + +import org.junit.Assert; +import org.junit.Test; + +/** + * @author tom.fennelly@gmail.com + */ +public class ImageNameTokensTest { + + @Test + public void test_no_tag() { + ImageNameTokens name = new ImageNameTokens("busybox"); + + Assert.assertEquals("busybox", name.userAndRepo); + Assert.assertEquals("latest", name.tag); + } + + @Test + public void test_empty_tag() { + ImageNameTokens name = new ImageNameTokens("busybox:"); + + Assert.assertEquals("busybox", name.userAndRepo); + Assert.assertEquals("latest", name.tag); + } + + @Test + public void test_with_tag() { + ImageNameTokens name = new ImageNameTokens("busybox:staging"); + + Assert.assertEquals("busybox", name.userAndRepo); + Assert.assertEquals("staging", name.tag); + + name = new ImageNameTokens("spring-petclinic:1"); + + Assert.assertEquals("spring-petclinic", name.userAndRepo); + Assert.assertEquals("1", name.tag); + + name = new ImageNameTokens("examplecorp/spring-petclinic:1"); + + Assert.assertEquals("examplecorp/spring-petclinic", name.userAndRepo); + Assert.assertEquals("1", name.tag); + } +} diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStepTest.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStepTest.java new file mode 100644 index 000000000..fec7d479a --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStepTest.java @@ -0,0 +1,72 @@ +/* + * The MIT License + * + * Copyright (c) 2015, CloudBees, 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. + */ +package org.jenkinsci.plugins.docker.workflow; + +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.CredentialsScope; +import com.cloudbees.plugins.credentials.common.IdCredentials; +import com.cloudbees.plugins.credentials.domains.Domain; +import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; +import java.util.Collections; +import java.util.Map; +import java.util.TreeMap; +import org.jenkinsci.plugins.docker.commons.credentials.DockerRegistryEndpoint; +import org.jenkinsci.plugins.workflow.BuildWatcher; +import org.jenkinsci.plugins.workflow.steps.StepConfigTester; +import org.jenkinsci.plugins.workflow.structs.DescribableHelper; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runners.model.Statement; +import org.jvnet.hudson.test.RestartableJenkinsRule; + +public class RegistryEndpointStepTest { + + @ClassRule public static BuildWatcher buildWatcher = new BuildWatcher(); + @Rule public RestartableJenkinsRule story = new RestartableJenkinsRule(); + + @Test public void configRoundTrip() { + story.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + IdCredentials registryCredentials = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "registryCreds", null, "me", "pass"); + CredentialsProvider.lookupStores(story.j.jenkins).iterator().next().addCredentials(Domain.global(), registryCredentials); + StepConfigTester sct = new StepConfigTester(story.j); + Map registryConfig = new TreeMap(); + registryConfig.put("url", "https://docker.my.com/"); + registryConfig.put("credentialsId", registryCredentials.getId()); + Map config = Collections.singletonMap("registry", registryConfig); + RegistryEndpointStep step = DescribableHelper.instantiate(RegistryEndpointStep.class, config); + step = sct.configRoundTrip(step); + DockerRegistryEndpoint registry = step.getRegistry(); + assertNotNull(registry); + assertEquals("https://docker.my.com/", registry.getUrl()); + assertEquals(registryCredentials.getId(), registry.getCredentialsId()); + assertEquals(config, DescribableHelper.uninstantiate(step)); + } + }); + } + +} \ No newline at end of file diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/ServerEndpointStepTest.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/ServerEndpointStepTest.java new file mode 100644 index 000000000..368df67bb --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/ServerEndpointStepTest.java @@ -0,0 +1,101 @@ +/* + * The MIT License + * + * Copyright (c) 2015, CloudBees, 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. + */ +package org.jenkinsci.plugins.docker.workflow; + +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.CredentialsScope; +import com.cloudbees.plugins.credentials.common.IdCredentials; +import com.cloudbees.plugins.credentials.domains.Domain; +import java.util.Collections; +import java.util.Map; +import java.util.TreeMap; +import org.jenkinsci.plugins.docker.commons.credentials.DockerServerCredentials; +import org.jenkinsci.plugins.docker.commons.credentials.DockerServerEndpoint; +import org.jenkinsci.plugins.workflow.BuildWatcher; +import org.jenkinsci.plugins.workflow.JenkinsRuleExt; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.jenkinsci.plugins.workflow.steps.StepConfigTester; +import org.jenkinsci.plugins.workflow.structs.DescribableHelper; +import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.runners.model.Statement; +import org.jvnet.hudson.test.RestartableJenkinsRule; + +public class ServerEndpointStepTest { + + @ClassRule public static BuildWatcher buildWatcher = new BuildWatcher(); + @Rule public RestartableJenkinsRule story = new RestartableJenkinsRule(); + + @Test public void configRoundTrip() { + story.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + IdCredentials serverCredentials = new DockerServerCredentials(CredentialsScope.GLOBAL, "serverCreds", null, "clientKey", "clientCertificate", "serverCaCertificate"); + CredentialsProvider.lookupStores(story.j.jenkins).iterator().next().addCredentials(Domain.global(), serverCredentials); + StepConfigTester sct = new StepConfigTester(story.j); + Map serverConfig = new TreeMap(); + serverConfig.put("uri", "tcp://host:2375"); + serverConfig.put("credentialsId", serverCredentials.getId()); + Map config = Collections.singletonMap("server", serverConfig); + ServerEndpointStep step = DescribableHelper.instantiate(ServerEndpointStep.class, config); + step = sct.configRoundTrip(step); + DockerServerEndpoint server = step.getServer(); + assertNotNull(server); + assertEquals("tcp://host:2375", server.getUri()); + assertEquals(serverCredentials.getId(), server.getCredentialsId()); + assertEquals(config, DescribableHelper.uninstantiate(step)); + } + }); + } + + @Test public void variables() { + story.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node {\n" + + " withDockerServer(server: [uri: 'tcp://host:1234']) {\n" + + " semaphore 'wait'\n" + + " sh 'echo would be connecting to $DOCKER_HOST'\n" + + " }\n" + + "}", true)); + WorkflowRun b = p.scheduleBuild2(0).waitForStart(); + SemaphoreStep.waitForStart("wait/1", b); + } + }); + story.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + SemaphoreStep.success("wait/1", null); + WorkflowJob p = story.j.jenkins.getItemByFullName("p", WorkflowJob.class); + WorkflowRun b = p.getLastBuild(); + story.j.assertLogContains("would be connecting to tcp://host:1234", story.j.assertBuildStatusSuccess(JenkinsRuleExt.waitForCompletion(b))); + } + }); + } + +} diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/WithContainerStepTest.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/WithContainerStepTest.java new file mode 100644 index 000000000..607537a52 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/WithContainerStepTest.java @@ -0,0 +1,124 @@ +/* + * The MIT License + * + * Copyright (c) 2015, CloudBees, 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. + */ +package org.jenkinsci.plugins.docker.workflow; + +import hudson.model.Result; +import hudson.tools.ToolProperty; +import java.util.Collections; +import org.jenkinsci.plugins.docker.commons.tools.DockerTool; +import org.jenkinsci.plugins.workflow.BuildWatcher; +import org.jenkinsci.plugins.workflow.JenkinsRuleExt; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.jenkinsci.plugins.workflow.steps.StepConfigTester; +import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.Rule; +import org.junit.runners.model.Statement; +import org.jvnet.hudson.test.RestartableJenkinsRule; + +public class WithContainerStepTest { + + @ClassRule public static BuildWatcher buildWatcher = new BuildWatcher(); + @Rule public RestartableJenkinsRule story = new RestartableJenkinsRule(); + + @Test public void configRoundTrip() { + story.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + WithContainerStep s1 = new WithContainerStep("java"); + s1.setArgs("--link db:db"); + story.j.assertEqualDataBoundBeans(s1, new StepConfigTester(story.j).configRoundTrip(s1)); + story.j.jenkins.getDescriptorByType(DockerTool.DescriptorImpl.class).setInstallations(new DockerTool("docker15", "/usr/local/docker15", Collections.>emptyList())); + s1.setToolName("docker15"); + story.j.assertEqualDataBoundBeans(s1, new StepConfigTester(story.j).configRoundTrip(s1)); + } + }); + } + + @Test public void basics() { + story.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + DockerTestUtil.assumeDocker(); + WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node {\n" + + " withDockerContainer('httpd:2.4.12') {\n" + + " sh 'cp /usr/local/apache2/conf/extra/httpd-userdir.conf .; ls -la'\n" + + " }\n" + + " sh 'ls -la; cat *.conf'\n" + + "}", true)); + WorkflowRun b = story.j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + story.j.assertLogContains("Require method GET POST OPTIONS", b); + } + }); + } + + @Test public void stop() { + story.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + DockerTestUtil.assumeDocker(); + WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node {\n" + + " withDockerContainer('httpd:2.4.12') {\n" + + " sh 'echo sleeping now with JENKINS_SERVER_COOKIE=$JENKINS_SERVER_COOKIE; sleep 999'\n" + + " }\n" + + "}", true)); + WorkflowRun b = p.scheduleBuild2(0).waitForStart(); + JenkinsRuleExt.waitForMessage("sleeping now", b); + b.doStop(); + story.j.assertBuildStatus(Result.ABORTED, JenkinsRuleExt.waitForCompletion(b)); + } + }); + } + + @Test public void restart() { + story.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + DockerTestUtil.assumeDocker(); + WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node {\n" + + " withDockerContainer('httpd:2.4.12') {\n" + + " semaphore 'wait'\n" + + " sh 'cat /usr/local/apache2/conf/extra/httpd-userdir.conf'\n" + + " }\n" + + "}", true)); + WorkflowRun b = p.scheduleBuild2(0).waitForStart(); + SemaphoreStep.waitForStart("wait/1", b); + } + }); + story.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + SemaphoreStep.success("wait/1", null); + WorkflowJob p = story.j.jenkins.getItemByFullName("p", WorkflowJob.class); + WorkflowRun b = p.getLastBuild(); + story.j.assertLogContains("Require method GET POST OPTIONS", story.j.assertBuildStatusSuccess(JenkinsRuleExt.waitForCompletion(b))); + } + }); + } + +} diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/client/DockerClientTest.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/client/DockerClientTest.java new file mode 100644 index 000000000..056bac441 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/client/DockerClientTest.java @@ -0,0 +1,108 @@ +/* + * The MIT License + * + * Copyright (c) 2015, CloudBees, 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. + */ +package org.jenkinsci.plugins.docker.workflow.client; + +import org.jenkinsci.plugins.docker.workflow.DockerTestUtil; +import hudson.EnvVars; +import hudson.Launcher; +import hudson.model.TaskListener; +import hudson.util.StreamTaskListener; +import hudson.util.VersionNumber; +import org.jenkinsci.plugins.docker.commons.fingerprint.ContainerRecord; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.Collections; + +/** + * @author tom.fennelly@gmail.com + */ +public class DockerClientTest { + + private DockerClient dockerClient; + + @Before + public void setup() throws Exception { + DockerTestUtil.assumeDocker(); + + // Set stuff up for the test + TaskListener taskListener = StreamTaskListener.fromStderr(); + Launcher.LocalLauncher launcher = new Launcher.LocalLauncher(taskListener); + + dockerClient = new DockerClient(launcher, null, null); + } + + @Test + public void test_run() throws IOException, InterruptedException { + EnvVars launchEnv = newLaunchEnv(); + String containerId = + dockerClient.run(launchEnv, "learn/tutorial", null, null, Collections.emptyMap(), new EnvVars(), + dockerClient.whoAmI(), "echo", "hello world"); + Assert.assertEquals(64, containerId.length()); + ContainerRecord containerRecord = dockerClient.getContainerRecord(launchEnv, containerId); + Assert.assertEquals("8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c", containerRecord.getImageId()); + Assert.assertTrue(containerRecord.getContainerName().length() > 0); + Assert.assertTrue(containerRecord.getHost().length() > 0); + Assert.assertTrue(containerRecord.getCreated() > 1000000000000L); + + // Also test that the stop works and cleans up after itself + Assert.assertNotNull(dockerClient.inspect(launchEnv, containerId, ".Name")); + dockerClient.stop(launchEnv, containerId); + Assert.assertNull(dockerClient.inspect(launchEnv, containerId, ".Name")); + } + + @Test + public void test_valid_version() { + VersionNumber dockerVersion = DockerClient.parseVersionNumber("Docker version 1.5.0, build a8a31ef"); + Assert.assertFalse(dockerVersion.isOlderThan(new VersionNumber("1.1"))); + Assert.assertFalse(dockerVersion.isOlderThan(new VersionNumber("1.5"))); + Assert.assertTrue(dockerVersion.isOlderThan(new VersionNumber("1.10"))); + } + + @Test + public void test_invalid_version() { + Assert.assertNull(DockerClient.parseVersionNumber("xxx")); + } + + private EnvVars newLaunchEnv() { + // Create the KeyMaterial for connecting to the docker host/server. + // E.g. currently need to add something like the following to your env + // -DDOCKER_HOST_FOR_TEST="tcp://192.168.x.y:2376" + // -DDOCKER_HOST_KEY_DIR_FOR_TEST="/Users/tfennelly/.boot2docker/certs/boot2docker-vm" + final String docker_host_for_test = System.getProperty("DOCKER_HOST_FOR_TEST"); + final String docker_host_key_dir_for_test = System.getProperty("DOCKER_HOST_KEY_DIR_FOR_TEST"); + + EnvVars env = new EnvVars(); + if (docker_host_for_test != null) { + env.put("DOCKER_HOST", docker_host_for_test); + } + if (docker_host_key_dir_for_test != null) { + env.put("DOCKER_TLS_VERIFY", "1"); + env.put("DOCKER_CERT_PATH", docker_host_key_dir_for_test); + } + return env; + } +}