all() {
14 | return Jenkins.getInstance().getDescriptorList(ContainerDefinition.class);
15 | }
16 |
17 | public boolean canBeUsedForMainContainer() {
18 | return true;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/hints/VolumeHint.java:
--------------------------------------------------------------------------------
1 | package it.dockins.dockerslaves.hints;
2 |
3 | import it.dockins.dockerslaves.spec.Hint;
4 |
5 | /**
6 | * This hint has no descriptor as we don't want to see it exposed to end user, this is just a hack being used by
7 | * ${@link it.dockins.dockerslaves.spec.DockerSocketContainerDefinition}.
8 | * @author Nicolas De Loof
9 | */
10 | public class VolumeHint extends Hint{
11 |
12 | private final String volume;
13 |
14 | public VolumeHint(String volume) {
15 | this.volume = volume;
16 | }
17 |
18 | public String getVolume() {
19 | return volume;
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/trampoline/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: default clean build
2 |
3 | GO=$(GOROOT)/bin/go
4 | GOOS ?= $(shell GOROOT=$(GOROOT) $(GO) env GOOS)
5 | GOARCH ?= $(shell GOROOT=$(GOROOT) $(GO) env GOARCH)
6 | DISTDIR ?= $(CURDIR)/dist/$(GOOS)-$(GOARCH)
7 | GO_BUILDFLAGS ?= -x -v
8 |
9 | VERSION ?= 1.0
10 | REV ?= $(shell git rev-parse --short HEAD)
11 |
12 | ifdef VERSION
13 | GO_LDFLAGS := $(GO_LDFLAGS) -X main.Version=$(VERSION)
14 | endif
15 | ifdef REV
16 | GO_LDFLAGS := $(GO_LDFLAGS) -X main.CommitID=$(REV)
17 | endif
18 |
19 | default: build
20 |
21 | clean:
22 | rm -rf $(DISTDIR)
23 |
24 | build: $(DISTDIR)/trampoline
25 |
26 | $(DISTDIR)/%:
27 | GOPATH=$(CURDIR) GOOS=$(GOOS) GOARCH=$(GOARCH) $(GO) build $(GO_BUILDFLAGS) -ldflags "$(GO_LDFLAGS)" -o $(DISTDIR)/$*.fat $*
28 | upx -o $(DISTDIR)/$* $(DISTDIR)/$*.fat
29 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/hints/MemoryHint.java:
--------------------------------------------------------------------------------
1 | package it.dockins.dockerslaves.hints;
2 |
3 | import hudson.Extension;
4 | import it.dockins.dockerslaves.spec.Hint;
5 | import it.dockins.dockerslaves.spec.HintDescriptor;
6 | import org.kohsuke.stapler.DataBoundConstructor;
7 |
8 | /**
9 | * @author Nicolas De Loof
10 | */
11 | public class MemoryHint extends Hint{
12 |
13 | private final String memory;
14 |
15 | @DataBoundConstructor
16 | public MemoryHint(String memory) {
17 | this.memory = memory;
18 | }
19 |
20 | public String getMemory() {
21 | return memory;
22 | }
23 |
24 | @Extension
25 | public static class DescriptorImpl extends HintDescriptor {
26 |
27 | @Override
28 | public String getDisplayName() {
29 | return "Memory";
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Docker.md:
--------------------------------------------------------------------------------
1 | # Container-slaves Docker implementation
2 |
3 | Plugin is relying on Docker for container management.
4 |
5 | Remoting is established with a fixed image `jenkinsci/slave`.
6 | To prevent classes version mismatch, we use `docker cp` at startup to inject the remoting jar bundled with Jenkins into the
7 | container.
8 |
9 | `TMPDIR` and `java.io.tmpdir` are set to `/home/jenkins/.tmp` so every build file should be created within `/home/jenkins`.
10 |
11 | `/home/jenkins` is set as VOLUME, so it can be reused for a subsequent build, or browsed after the build
12 | as long as the container isn't removed.
13 |
14 | Build commands are run inside arbitrary containers, ran as user `jenkins` (uid:10000, gid:10000) so there's no permission issue accessing the
15 | workspace and other files inside `/home/jenkins`. This user is automatically created in container user do configure for the build.
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/spi/DockerHostSource.java:
--------------------------------------------------------------------------------
1 | package it.dockins.dockerslaves.spi;
2 |
3 | import hudson.ExtensionPoint;
4 | import hudson.model.AbstractDescribableImpl;
5 | import hudson.model.Job;
6 |
7 | import java.io.IOException;
8 |
9 | /**
10 | * A DockerHostSource is responsible to determine (or provision) the dockerhost to host a build for the specified job.
11 | *
12 | * Implementation can use this extension to execute some decision and/or provisioning logic, depending on infrastructure
13 | * details and constraints.
14 | *
15 | * @author Nicolas De Loof
16 | */
17 | public abstract class DockerHostSource extends AbstractDescribableImpl implements ExtensionPoint{
18 |
19 | /**
20 | * Allocate / Determine best Docker host to use to build this Job.
21 | * @param job
22 | * @return
23 | */
24 | public abstract DockerHostConfig getDockerHost(Job job) throws IOException, InterruptedException;
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/ContainerSpecEnvironmentContributor.java:
--------------------------------------------------------------------------------
1 | package it.dockins.dockerslaves;
2 |
3 | import hudson.EnvVars;
4 | import hudson.Extension;
5 | import hudson.model.EnvironmentContributor;
6 | import hudson.model.Job;
7 | import hudson.model.Run;
8 | import hudson.model.TaskListener;
9 | import it.dockins.dockerslaves.spec.ContainerSetDefinition;
10 | import it.dockins.dockerslaves.spec.SideContainerDefinition;
11 |
12 | import javax.annotation.Nonnull;
13 | import java.io.IOException;
14 |
15 | /**
16 | * @author Nicolas De Loof
17 | */
18 | @Extension
19 | public class ContainerSpecEnvironmentContributor extends EnvironmentContributor {
20 |
21 | @Override
22 | public void buildEnvironmentFor(@Nonnull Run r, @Nonnull EnvVars envs, @Nonnull TaskListener listener) throws IOException, InterruptedException {
23 | final Job job = r.getParent();
24 | final ContainerSetDefinition property = (ContainerSetDefinition) job.getProperty(ContainerSetDefinition.class);
25 | if (property == null) return;
26 |
27 | property.getBuildHostImage().setupEnvironment(envs);
28 | for (SideContainerDefinition sidecar : property.getSideContainers()) {
29 | sidecar.getSpec().setupEnvironment(envs);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/spi/DockerProvisionerFactory.java:
--------------------------------------------------------------------------------
1 | package it.dockins.dockerslaves.spi;
2 |
3 | import hudson.model.AbstractDescribableImpl;
4 | import hudson.model.Job;
5 | import it.dockins.dockerslaves.spec.ContainerDefinitionDescriptor;
6 | import it.dockins.dockerslaves.spec.ContainerSetDefinition;
7 | import it.dockins.dockerslaves.spec.DockerSocketContainerDefinition;
8 |
9 | import java.io.IOException;
10 |
11 | /**
12 | * This component is responsible to orchestrate the provisioning of a build environment based on configured
13 | * {@link ContainerSetDefinition} for specified {@link Job}.
14 | *
15 | * @author Nicolas De Loof
16 | */
17 | public abstract class DockerProvisionerFactory extends AbstractDescribableImpl {
18 |
19 | public abstract DockerProvisioner createProvisionerForClassicJob(Job job, ContainerSetDefinition spec) throws IOException, InterruptedException;
20 |
21 | public abstract DockerProvisioner createProvisionerForPipeline(Job job, ContainerSetDefinition spec) throws IOException, InterruptedException;
22 |
23 | public boolean canBeUsedAsMainContainer(ContainerDefinitionDescriptor d) {
24 | return d.clazz != DockerSocketContainerDefinition.class;
25 | }
26 |
27 | public boolean canBeUsedAsSideContainer(ContainerDefinitionDescriptor d) {
28 | return true;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/resources/it/dockins/dockerslaves/spec/DockerSocketContainerDefinition/config.jelly:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/trampoline/src/trampoline/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "os/exec"
8 | "os/signal"
9 | "syscall"
10 | )
11 |
12 | var (
13 | // Version is a state variable, written at the link stage. See Makefile.
14 | Version string
15 | // CommitID is a state variable, written at the link stage. See Makefile.
16 | CommitID string
17 | )
18 |
19 | func usage() {
20 | fmt.Println(`usage: trampoline
21 |
22 | Where subcommand can be:
23 | cdexec: Run a command after changing current working directory to the given directory
24 | `)
25 | }
26 |
27 | func cdexec(args []string) {
28 | if len(args) < 2 {
29 | usage()
30 | os.Exit(1)
31 | }
32 |
33 | err := os.Chdir(args[0])
34 | if err != nil {
35 | log.Fatal(err)
36 | os.Exit(255)
37 | }
38 |
39 | binary, lookErr := exec.LookPath(args[1])
40 | if lookErr != nil {
41 | log.Fatal(lookErr)
42 | os.Exit(255)
43 | }
44 |
45 | if err := syscall.Exec(binary, args[1:], os.Environ()); err != nil {
46 | log.Fatal(err)
47 | os.Exit(255)
48 | }
49 | }
50 |
51 | func wait_system_signal() {
52 | channel := make(chan os.Signal)
53 | signal.Notify(channel)
54 | <-channel
55 | }
56 |
57 | func main() {
58 | if len(os.Args) < 2 {
59 | usage()
60 | os.Exit(1)
61 | }
62 |
63 | subCommand := os.Args[1]
64 | commandLine := os.Args[2:]
65 |
66 | switch subCommand {
67 | case "cdexec":
68 | cdexec(commandLine)
69 | case "wait":
70 | wait_system_signal()
71 | default:
72 | usage()
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/main/resources/it/dockins/dockerslaves/spec/ContainerSetDefinition/help-sideContainers.html:
--------------------------------------------------------------------------------
1 |
25 |
26 | Your build can rely on side images to host build resources, like a test database or application server to
27 | run the application you are building and testing.
--------------------------------------------------------------------------------
/src/main/resources/index.jelly:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
30 |
31 | Uses
Docker containers to run Jenkins build agents.
32 |
33 |
--------------------------------------------------------------------------------
/src/main/resources/it/dockins/dockerslaves/spec/ImageIdContainerDefinition/help-forcePull.html:
--------------------------------------------------------------------------------
1 |
25 |
26 | Always pull the image before launching the container, to ensure the latest is ran during the build.
27 | Useful if you use a latest image. Will slow down the build, but ensure you always run the up-to-date image.
28 |
--------------------------------------------------------------------------------
/src/main/resources/it/dockins/dockerslaves/DefaultDockerHostSource/config.jelly:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/api/OneShotExecutorProvisioningException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2016, CloudBees, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | *
24 | */
25 | package it.dockins.dockerslaves.api;
26 |
27 | public class OneShotExecutorProvisioningException extends RuntimeException {
28 | public OneShotExecutorProvisioningException() {
29 | }
30 |
31 | public OneShotExecutorProvisioningException(Exception cause) {
32 | super(cause);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/resources/it/dockins/dockerslaves/ContainersContext/badge.jelly:
--------------------------------------------------------------------------------
1 |
2 |
25 |
26 |
27 |
28 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/main/resources/it/dockins/dockerslaves/hints/MemoryHint/config.jelly:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/main/resources/it/dockins/dockerslaves/DefaultDockerProvisionerFactory/help-remotingImage.html:
--------------------------------------------------------------------------------
1 |
25 | To support Jenkins internal plumbing, docker-slaves do run a dedicated container for jenkins remoting.
26 | You can override the image in used for this purpose with this option, anyway we recommend to use the official
27 | jenkins/slave.
28 |
--------------------------------------------------------------------------------
/src/main/resources/it/dockins/dockerslaves/drivers/PlainDockerAPIDockerDriverFactory/config.jelly:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/main/resources/it/dockins/dockerslaves/pipeline/DockerNodeStep/config.jelly:
--------------------------------------------------------------------------------
1 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/main/resources/it/dockins/dockerslaves/DockerSlaves/help-defaultBuildContainerImageName.html:
--------------------------------------------------------------------------------
1 |
25 | Docker image used to host the build environment and run build steps if none is configured at job level.
26 | If you don't want development teams to manage build environment images, but still benefits from docker isolated builds
27 | and test resources containers, you can prepare a generic toolbox image and use it here.
28 |
29 |
--------------------------------------------------------------------------------
/src/main/resources/it/dockins/dockerslaves/spec/SideContainerDefinition/config.jelly:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/main/resources/it/dockins/dockerslaves/DefaultDockerProvisionerFactory/help-scmImage.html:
--------------------------------------------------------------------------------
1 |
25 | As all build steps, the SCM checkout commands are ran inside a container. This option let you configure a docker image
26 | which has the adequate SCM tool installed. buildpack-deps:scm we recommend by default do support git,
27 | mercurial, bazaar and subversion. As it is an ancestry for the jenkinsci/slave image, you probably already
28 | have this image available on your Dockerhost.
--------------------------------------------------------------------------------
/src/main/resources/it/dockins/dockerslaves/spec/ImageIdContainerDefinition/configure-entries.jelly:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/main/resources/it/dockins/dockerslaves/spec/ContainerSetDefinition/help-buildHostImage.html:
--------------------------------------------------------------------------------
1 |
25 | Docker image used to host the build environment and run build steps.
26 | Using this option, you have full control on your build environment to make it fully reproducible and isolated
27 | from other builds.
28 | You can use arbitrary docker images here, setup with the builds tools required by your job.
29 | There's no prerequisites on the docker image as the jenkins infrastructure setup is handled transparently.
30 |
31 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/spi/DockerProvisioner.java:
--------------------------------------------------------------------------------
1 | package it.dockins.dockerslaves.spi;
2 |
3 | import hudson.Launcher;
4 | import hudson.Proc;
5 | import hudson.model.TaskListener;
6 | import it.dockins.dockerslaves.Container;
7 | import it.dockins.dockerslaves.ContainersContext;
8 | import it.dockins.dockerslaves.DockerComputer;
9 |
10 | import java.io.IOException;
11 |
12 | /**
13 | * @author Nicolas De Loof
14 | */
15 | public abstract class DockerProvisioner {
16 |
17 | public abstract ContainersContext getContext();
18 |
19 | /**
20 | * Launch a container to host jenkins remoting agent and establish a channel as a Jenkins slave.
21 | */
22 | public abstract Container launchRemotingContainer(DockerComputer computer, TaskListener listener) throws IOException, InterruptedException;
23 |
24 | /**
25 | * Launch a container with adequate tools to run the SCM checkout build phase.
26 | */
27 | public abstract Container launchScmContainer(TaskListener listener) throws IOException, InterruptedException;
28 |
29 | /**
30 | * Launch build environment as defined by (@link Job}'s {@link it.dockins.dockerslaves.spec.ContainerSetDefinition}.
31 | */
32 | public abstract Container launchBuildContainers(Launcher.ProcStarter starter, TaskListener listener) throws IOException, InterruptedException;
33 |
34 | /**
35 | * Run specified process inside the main build container
36 | */
37 | public abstract Proc launchBuildProcess(Launcher.ProcStarter procStarter, TaskListener listener) throws IOException, InterruptedException;
38 |
39 | /**
40 | * Cleanup all allocated resources
41 | */
42 | public abstract void clean(TaskListener listener) throws IOException, InterruptedException;
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/spi/DockerHostConfig.java:
--------------------------------------------------------------------------------
1 | package it.dockins.dockerslaves.spi;
2 |
3 | import hudson.EnvVars;
4 | import hudson.FilePath;
5 | import hudson.model.Item;
6 | import hudson.security.ACL;
7 | import hudson.security.ACLContext;
8 | import org.jenkinsci.plugins.docker.commons.credentials.DockerServerEndpoint;
9 | import org.jenkinsci.plugins.docker.commons.credentials.KeyMaterial;
10 |
11 | import java.io.Closeable;
12 | import java.io.IOException;
13 |
14 | /**
15 | * Configuration options used to access a specific (maybe dedicated to a build) Docker Host.
16 | *
17 | * Intent here is to allow some infrastructure plugin to prepare a dedicated Docker Host per build,
18 | * using some higher level isolation, so the build is safe to do whatever it needs with it's docker
19 | * daemon without risk to impact other builds.
20 | *
21 | * @author Nicolas De Loof
22 | */
23 | public class DockerHostConfig implements Closeable {
24 |
25 | /** Docker Host's daemon endpoint */
26 | private final DockerServerEndpoint endpoint;
27 |
28 | /** Docker API access keys */
29 | private final KeyMaterial keys;
30 |
31 | public DockerHostConfig(DockerServerEndpoint endpoint, Item context) throws IOException, InterruptedException {
32 | this.endpoint = endpoint;
33 | try (ACLContext oldContext = ACL.as(ACL.SYSTEM)) {
34 | keys = endpoint.newKeyMaterialFactory(context, FilePath.localChannel).materialize();
35 | }
36 | }
37 |
38 | public DockerServerEndpoint getEndpoint() {
39 | return endpoint;
40 | }
41 |
42 | public EnvVars getEnvironment() {
43 | return keys.env();
44 | }
45 |
46 | @Override
47 | public void close() throws IOException {
48 | keys.close();
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/resources/it/dockins/dockerslaves/spec/ContainerDefinition/config.jelly:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/DefaultDockerHostSource.java:
--------------------------------------------------------------------------------
1 | package it.dockins.dockerslaves;
2 |
3 | import hudson.Extension;
4 | import hudson.model.Job;
5 | import it.dockins.dockerslaves.spi.DockerHostConfig;
6 | import org.jenkinsci.plugins.docker.commons.credentials.DockerServerEndpoint;
7 | import org.kohsuke.stapler.DataBoundConstructor;
8 | import it.dockins.dockerslaves.spi.DockerHostSource;
9 | import it.dockins.dockerslaves.spi.DockerHostSourceDescriptor;
10 |
11 | import javax.annotation.Nonnull;
12 | import java.io.IOException;
13 |
14 | /**
15 | * @author Nicolas De Loof
16 | */
17 | public class DefaultDockerHostSource extends DockerHostSource {
18 |
19 | private final DockerServerEndpoint dockerServerEndpoint;
20 | public static final DockerServerEndpoint DEFAULT = new DockerServerEndpoint(null, null);
21 |
22 | public DefaultDockerHostSource() {
23 | this(new DockerServerEndpoint(null, null));
24 | }
25 |
26 | @DataBoundConstructor
27 | public DefaultDockerHostSource(DockerServerEndpoint dockerServerEndpoint) {
28 | this.dockerServerEndpoint = dockerServerEndpoint;
29 | }
30 |
31 | public DockerServerEndpoint getDockerServerEndpoint() {
32 |
33 | return dockerServerEndpoint != null ? dockerServerEndpoint : DEFAULT;
34 | }
35 |
36 | @Override
37 | public DockerHostConfig getDockerHost(Job job) throws IOException, InterruptedException {
38 | return new DockerHostConfig(getDockerServerEndpoint(), job);
39 | }
40 |
41 | @Extension
42 | public static class DescriptorImpl extends DockerHostSourceDescriptor {
43 |
44 | @Nonnull
45 | @Override
46 | public String getDisplayName() {
47 | return "Standalone Docker daemon / Docker Swarm cluster";
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/resources/it/dockins/dockerslaves/spec/DockerfileContainerDefinition/configure-entries.jelly:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/Architecture.md:
--------------------------------------------------------------------------------
1 | # Architecture
2 |
3 | ## Why not Cloud API ?
4 |
5 | Jenkins `hudson.slaves.Cloud` is designed to manage virtual machines, i.e. heavy to bootstrap, (relatively) long lived.
6 | As we manage containers, which are fast to start (as long as image is available on host) and designed to only host a single build,
7 | the API doesn't strictly match our needs.
8 |
9 | ## Build Pod
10 |
11 | The Jenkins slave is created as a set of containers, a.k.a. "pod", oen of them being used to establish the jenkins remoting
12 | communication channel, the others to run build steps, sharing volumes and network. This set of container is internally documented
13 | as `com.cloudbees.jenkins.plugins.containerslaves.DockerBuildContext`.
14 |
15 | ## Provisioner
16 |
17 | Jenkins relies on `hudson.slaves.NodeProvisioner` to determine if and when a new executor has to be created by the
18 | `hudson.slaves.Cloud` implementation. This introduces unnecessary delay provisioning our container slave (pod).
19 | As a workaround, we register a custom `hudson.model.queue.QueueListener` to be notified just as a job enter the build queue,
20 | then can create the required containers without delay. It also assign a unique `hudson.model.Label`, to ensure this container
21 | will only run once and assigned to this exact build.
22 |
23 | ## Container Provisioner
24 |
25 | `com.cloudbees.jenkins.plugins.containerslaves.DockerProvisioner` is responsible to create the build pob. For this purpose it
26 | relies on a single, common, slave remoting image. `com.cloudbees.jenkins.plugins.containerslaves.DockerDriver` do handle the
27 | technical details
28 |
29 | ## Security
30 |
31 | We arbitrary choose to launch command inside containers with UNIX user `jenkins` (uid 10000, gid 100) which is defined in the remoting container image.
32 |
33 | To use this user in unknown containers images, we use `docker cp` to update `/etc/passwd` and `/etc/group` (so commands like `id -a ` work as expected).
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/ProvisionScheduler.java:
--------------------------------------------------------------------------------
1 | package it.dockins.dockerslaves;
2 |
3 | import hudson.Extension;
4 | import hudson.model.AbstractProject;
5 | import hudson.model.Node;
6 | import hudson.model.Queue;
7 | import hudson.model.queue.CauseOfBlockage;
8 | import hudson.model.queue.QueueTaskDispatcher;
9 | import it.dockins.dockerslaves.spec.ContainerSetDefinition;
10 | import jenkins.model.Jenkins;
11 |
12 | /**
13 | * Responsible for allowing tasks to go into buildable state.
14 | */
15 | @Extension
16 | public class ProvisionScheduler extends QueueTaskDispatcher {
17 |
18 | @Override
19 | public CauseOfBlockage canRun(Queue.Item item) {
20 | if (item.task instanceof AbstractProject) {
21 | AbstractProject job = (AbstractProject) item.task;
22 | ContainerSetDefinition def = (ContainerSetDefinition) job.getProperty(ContainerSetDefinition.class);
23 | if (def == null) {
24 | return null;
25 | }
26 |
27 | int slaveCount = 0;
28 | DockerSlaves plugin = DockerSlaves.get();
29 | for (Node node : Jenkins.getInstance().getNodes()) {
30 | if (node instanceof DockerSlave) {
31 | if (((DockerSlave)node).getQueueItemId() == item.getId()) {
32 | return null;
33 | }
34 | slaveCount++;
35 | }
36 | }
37 |
38 | if (slaveCount >= plugin.getMaxSlaves()) {
39 | return new WaitForADockerSlot();
40 | } else {
41 | return null;
42 | }
43 | } else {
44 | return null;
45 | }
46 | }
47 |
48 | static final class WaitForADockerSlot extends CauseOfBlockage {
49 | private WaitForADockerSlot() {
50 | }
51 |
52 | public String getShortDescription() {
53 | return "Waiting for a Docker slot";
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/drivers/PlainDockerAPIDockerDriverFactory.java:
--------------------------------------------------------------------------------
1 | package it.dockins.dockerslaves.drivers;
2 |
3 | import hudson.Extension;
4 | import hudson.model.Job;
5 | import it.dockins.dockerslaves.DefaultDockerHostSource;
6 | import it.dockins.dockerslaves.spi.DockerDriver;
7 | import it.dockins.dockerslaves.spi.DockerDriverFactory;
8 | import it.dockins.dockerslaves.spi.DockerDriverFactoryDescriptor;
9 | import it.dockins.dockerslaves.spi.DockerHostSource;
10 | import org.jenkinsci.plugins.docker.commons.credentials.DockerServerEndpoint;
11 | import org.kohsuke.stapler.DataBoundConstructor;
12 |
13 | import javax.annotation.Nonnull;
14 | import java.io.IOException;
15 |
16 | /**
17 | * A ${@link DockerDriverFactory} relying on plain good old Docker API usage.
18 | *
19 | * @author Nicolas De Loof
20 | */
21 | public class PlainDockerAPIDockerDriverFactory extends DockerDriverFactory {
22 |
23 | private final DockerHostSource dockerHostSource;
24 |
25 |
26 | @DataBoundConstructor
27 | public PlainDockerAPIDockerDriverFactory(DockerHostSource dockerHostSource) {
28 | this.dockerHostSource = dockerHostSource;
29 | }
30 |
31 | public PlainDockerAPIDockerDriverFactory(DockerServerEndpoint dockerHost) {
32 | this(new DefaultDockerHostSource(dockerHost));
33 | }
34 |
35 | public DockerHostSource getDockerHostSource() {
36 | return dockerHostSource;
37 | }
38 |
39 | @Override
40 | public DockerDriver forJob(Job context) throws IOException, InterruptedException {
41 | return new CliDockerDriver(dockerHostSource.getDockerHost(context));
42 | }
43 |
44 | @Extension
45 | public static class DescriptorImp extends DockerDriverFactoryDescriptor {
46 |
47 | @Nonnull
48 | @Override
49 | public String getDisplayName() {
50 | return "Docker CLI (require docker executable on PATH)";
51 | }
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/resources/it/dockins/dockerslaves/DockerSlaves/config.jelly:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/DockerSlaveAssignmentAction.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2015, CloudBees, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | *
24 | */
25 |
26 | package it.dockins.dockerslaves;
27 |
28 | import hudson.model.InvisibleAction;
29 | import hudson.model.Label;
30 | import hudson.model.labels.LabelAssignmentAction;
31 | import hudson.model.queue.SubTask;
32 | import jenkins.model.Jenkins;
33 |
34 | import javax.annotation.CheckForNull;
35 |
36 | public class DockerSlaveAssignmentAction extends InvisibleAction implements LabelAssignmentAction {
37 |
38 | private final String assignedNodeName;
39 |
40 | public DockerSlaveAssignmentAction(String assignedNodeName) {
41 | this.assignedNodeName = assignedNodeName;
42 | }
43 |
44 | public @CheckForNull
45 | DockerSlave getAssignedNodeName() {
46 | return (DockerSlave) Jenkins.getInstance().getNode(assignedNodeName);
47 | }
48 |
49 | @Override
50 | public Label getAssignedLabel(SubTask task) {
51 | return Label.get(assignedNodeName);
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/resources/it/dockins/dockerslaves/DefaultDockerProvisionerFactory/config.jelly:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
28 |
29 |
30 |
31 | Build executor will be created as a composition of linked Docker containers.
32 | This offers maximal flexibility on the build and side containers.
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/src/main/resources/it/dockins/dockerslaves/spec/ContainerSetDefinition/config.jelly:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/spec/ContainerDefinition.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2015, CloudBees, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | *
24 | */
25 |
26 | package it.dockins.dockerslaves.spec;
27 |
28 | import hudson.EnvVars;
29 | import hudson.FilePath;
30 | import hudson.model.AbstractDescribableImpl;
31 | import hudson.model.TaskListener;
32 | import it.dockins.dockerslaves.spi.DockerDriver;
33 | import org.kohsuke.stapler.DataBoundSetter;
34 |
35 | import java.io.IOException;
36 | import java.util.List;
37 |
38 | /**
39 | * @author Nicolas De Loof
40 | */
41 | public abstract class ContainerDefinition extends AbstractDescribableImpl {
42 |
43 | private List hints;
44 |
45 | public List getHints() {
46 | return hints;
47 | }
48 |
49 | @DataBoundSetter
50 | public void setHints(List hints) {
51 | this.hints = hints;
52 | }
53 |
54 | public abstract String getImage(DockerDriver driver, FilePath workspace, TaskListener listener) throws IOException, InterruptedException;
55 |
56 | public void setupEnvironment(EnvVars env) {}
57 | }
58 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/spec/SideContainerDefinition.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2015, CloudBees, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | *
24 | */
25 |
26 | package it.dockins.dockerslaves.spec;
27 |
28 | import hudson.Extension;
29 | import hudson.model.AbstractDescribableImpl;
30 | import hudson.model.Descriptor;
31 | import org.kohsuke.stapler.DataBoundConstructor;
32 |
33 | /**
34 | * @author Nicolas De Loof
35 | */
36 | public class SideContainerDefinition extends AbstractDescribableImpl {
37 |
38 | private final String name;
39 | private final ContainerDefinition spec;
40 |
41 | @DataBoundConstructor
42 | public SideContainerDefinition(String name, ContainerDefinition spec) {
43 | this.name = name;
44 | this.spec = spec;
45 | }
46 |
47 | public String getName() {
48 | return name;
49 | }
50 |
51 | public ContainerDefinition getSpec() {
52 | return spec;
53 | }
54 |
55 | @Extension
56 | public static class DescriptorImpl extends Descriptor {
57 |
58 | @Override
59 | public String getDisplayName() {
60 | return "Side Container";
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/main/resources/it/dockins/dockerslaves/ContainersContext/index.jelly:
--------------------------------------------------------------------------------
1 |
25 |
26 |
27 |
28 |
29 |
30 | resources used by this build
31 |
32 |
33 |
34 | Containers
35 |
36 | - jenkins agent : ${it.remotingContainer.id} (${it.remotingContainer.imageName})
37 | - main build container : ${it.buildContainer.id} (${it.buildContainer.imageName})
38 |
39 | - ${container.key} : ${container.value.id} (${container.value.imageName})
40 |
41 |
42 |
43 | Volumes
44 |
45 | - workspace : ${it.workdirVolume}
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/spi/DockerDriver.java:
--------------------------------------------------------------------------------
1 | package it.dockins.dockerslaves.spi;
2 |
3 | import hudson.Launcher;
4 | import hudson.Proc;
5 | import hudson.model.TaskListener;
6 | import it.dockins.dockerslaves.Container;
7 | import it.dockins.dockerslaves.DockerComputer;
8 | import it.dockins.dockerslaves.spec.Hint;
9 |
10 | import java.io.Closeable;
11 | import java.io.IOException;
12 | import java.util.List;
13 |
14 | /**
15 | * Manage Docker resources creation and access so docker-slaves can run a build.
16 | *
17 | * Implementation is responsible to adapt docker infrastructure APIs
18 | */
19 | public abstract class DockerDriver implements Closeable {
20 |
21 | public abstract boolean hasVolume(TaskListener listener, String name) throws IOException, InterruptedException;
22 |
23 | public abstract String createVolume(TaskListener listener) throws IOException, InterruptedException;
24 |
25 | public abstract boolean hasContainer(TaskListener listener, String id) throws IOException, InterruptedException;
26 |
27 | public abstract Container launchRemotingContainer(TaskListener listener, String image, String workdir, DockerComputer computer) throws IOException, InterruptedException;
28 |
29 | public abstract Container launchBuildContainer(TaskListener listener, String image, Container remotingContainer, List hints) throws IOException, InterruptedException;
30 |
31 | public abstract Container launchSideContainer(TaskListener listener, String image, Container remotingContainer, List hints) throws IOException, InterruptedException;
32 |
33 | public abstract Proc execInContainer(TaskListener listener, String containerId, Launcher.ProcStarter starter) throws IOException, InterruptedException;
34 |
35 | public abstract void removeContainer(TaskListener listener, Container instance) throws IOException, InterruptedException;
36 |
37 | public abstract void pullImage(TaskListener listener, String image) throws IOException, InterruptedException;
38 |
39 | public abstract boolean checkImageExists(TaskListener listener, String image) throws IOException, InterruptedException;
40 |
41 | public abstract void buildDockerfile(TaskListener listener, String dockerfilePath, String tag, boolean pull) throws IOException, InterruptedException;
42 |
43 | /**
44 | * Return server version string, used actually to check connectivity with backend
45 | */
46 | public abstract String serverVersion(TaskListener listener) throws IOException, InterruptedException;
47 | }
48 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/DockerComputerLauncher.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2015, CloudBees, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | *
24 | */
25 |
26 | package it.dockins.dockerslaves;
27 |
28 | import hudson.model.TaskListener;
29 | import hudson.slaves.ComputerLauncher;
30 | import hudson.slaves.SlaveComputer;
31 | import it.dockins.dockerslaves.spi.DockerProvisioner;
32 |
33 | import java.io.IOException;
34 | import java.util.logging.Logger;
35 |
36 | /**
37 | * Launches initials containers
38 | */
39 | public class DockerComputerLauncher extends ComputerLauncher {
40 |
41 | private static final Logger LOGGER = Logger.getLogger(DockerComputerLauncher.class.getName());
42 |
43 | public DockerComputerLauncher() {
44 | }
45 |
46 | @Override
47 | public void launch(final SlaveComputer computer, TaskListener listener) throws IOException, InterruptedException {
48 | if (computer instanceof DockerComputer) {
49 | launch((DockerComputer) computer, listener);
50 | } else {
51 | throw new IllegalArgumentException("This launcher only can handle DockerComputer");
52 | }
53 | }
54 |
55 | public void launch(final DockerComputer computer, TaskListener listener) throws IOException, InterruptedException {
56 | listener.getLogger().println("Start Docker container to host the build");
57 | DockerProvisioner provisioner = computer.getProvisioner();
58 | provisioner.launchRemotingContainer(computer, listener);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/DockerLauncher.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2015, CloudBees, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | *
24 | */
25 |
26 | package it.dockins.dockerslaves;
27 |
28 | import hudson.Launcher;
29 | import hudson.Proc;
30 | import hudson.model.TaskListener;
31 | import hudson.remoting.VirtualChannel;
32 | import it.dockins.dockerslaves.spi.DockerProvisioner;
33 |
34 | import java.io.IOException;
35 | import java.lang.Override;
36 | import java.util.logging.Logger;
37 |
38 | /**
39 | * Process launcher which uses docker exec instead of execve
40 | * Jenkins relies on remoting channel to run commands / process on executor. As Docker can as well be used to run a
41 | * process remotely, we can just bypass jenkins remoting.
42 | */
43 | public class DockerLauncher extends Launcher.DecoratedLauncher {
44 | private static final Logger LOGGER = Logger.getLogger(DockerLauncher.class.getName());
45 |
46 | private final DockerProvisioner provisioner;
47 |
48 | public DockerLauncher(TaskListener listener, VirtualChannel channel, boolean isUnix, DockerProvisioner provisioner) {
49 | super(new Launcher.RemoteLauncher(listener, channel, isUnix));
50 | this.provisioner = provisioner;
51 | }
52 |
53 | @Override
54 | public Proc launch(ProcStarter starter) throws IOException {
55 | try {
56 | return provisioner.launchBuildProcess(starter, listener);
57 | } catch (InterruptedException e) {
58 | throw new IOException(e);
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/spec/DockerSocketContainerDefinition.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2015, CloudBees, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | *
24 | */
25 |
26 | package it.dockins.dockerslaves.spec;
27 |
28 | import hudson.EnvVars;
29 | import hudson.Extension;
30 | import hudson.FilePath;
31 | import hudson.model.TaskListener;
32 | import it.dockins.dockerslaves.hints.VolumeHint;
33 | import it.dockins.dockerslaves.spi.DockerDriver;
34 | import org.kohsuke.stapler.DataBoundConstructor;
35 |
36 | import java.util.Collections;
37 | import java.util.List;
38 |
39 | /**
40 | * Configure a sidecar container to expose the host's docker socket inside container set,
41 | * @author Nicolas De Loof
42 | */
43 | public class DockerSocketContainerDefinition extends ContainerDefinition {
44 |
45 | @DataBoundConstructor
46 | public DockerSocketContainerDefinition() {
47 | }
48 |
49 | @Override
50 | public String getImage(DockerDriver driver, FilePath workspace, TaskListener listener) {
51 | return "dockins/dockersock";
52 | }
53 |
54 | @Override
55 | public List getHints() {
56 | return Collections.singletonList((Hint) new VolumeHint("/var/run/docker.sock:/var/run/docker.sock"));
57 | }
58 |
59 | @Override
60 | public void setupEnvironment(EnvVars env) {
61 | env.put("DOCKER_HOST", "tcp://localhost:2375");
62 | }
63 |
64 | @Extension(ordinal = -666)
65 | public static class DescriptorImpl extends ContainerDefinitionDescriptor {
66 |
67 | @Override
68 | public String getDisplayName() {
69 | return "Access host's docker daemon";
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/spec/ImageIdContainerDefinition.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2015, CloudBees, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | *
24 | */
25 |
26 | package it.dockins.dockerslaves.spec;
27 |
28 | import hudson.Extension;
29 | import hudson.FilePath;
30 | import hudson.model.TaskListener;
31 | import it.dockins.dockerslaves.spi.DockerDriver;
32 | import org.kohsuke.stapler.DataBoundConstructor;
33 |
34 | import java.io.IOException;
35 |
36 | /**
37 | * @author Nicolas De Loof
38 | */
39 | public class ImageIdContainerDefinition extends ContainerDefinition {
40 |
41 | private final String image;
42 |
43 | private final boolean forcePull;
44 |
45 | @DataBoundConstructor
46 | public ImageIdContainerDefinition(String image, boolean forcePull) {
47 | this.image = image;
48 | this.forcePull = forcePull;
49 | }
50 |
51 | public String getImage() {
52 | return image;
53 | }
54 |
55 | @Override
56 | public String getImage(DockerDriver driver, FilePath workspace, TaskListener listener) throws IOException, InterruptedException {
57 |
58 | boolean pull = forcePull;
59 | boolean result = driver.checkImageExists(listener, image);
60 |
61 | if (!result) {
62 | // Could be a docker failure, but most probably image isn't available
63 | pull = true;
64 | }
65 |
66 | if (pull) {
67 | listener.getLogger().println("Pulling docker image " + image);
68 | driver.pullImage(listener, image);
69 | }
70 |
71 | return image;
72 | }
73 |
74 | public boolean isForcePull() {
75 | return forcePull;
76 | }
77 |
78 | @Extension(ordinal = 99)
79 | public static class DescriptorImpl extends ContainerDefinitionDescriptor {
80 |
81 | @Override
82 | public String getDisplayName() {
83 | return "Docker image";
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/DockerComputer.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2015, CloudBees, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | *
24 | */
25 |
26 | package it.dockins.dockerslaves;
27 |
28 | import hudson.EnvVars;
29 | import hudson.model.Computer;
30 | import it.dockins.dockerslaves.api.OneShotComputer;
31 | import hudson.slaves.ComputerLauncher;
32 | import it.dockins.dockerslaves.spi.DockerProvisioner;
33 |
34 | import java.io.IOException;
35 | import java.util.logging.Logger;
36 |
37 | /**
38 | * A computer on which a specific build will occur
39 | */
40 | public class DockerComputer extends OneShotComputer {
41 |
42 | private final DockerSlave slave;
43 |
44 | private final DockerProvisioner provisioner;
45 |
46 | public DockerComputer(DockerSlave slave, DockerProvisioner provisioner) {
47 | super(slave);
48 | this.provisioner = provisioner;
49 | this.slave = slave;
50 | }
51 |
52 | @Override
53 | public DockerSlave getNode() {
54 | return slave;
55 | }
56 |
57 | @Override
58 | public Boolean isUnix() {
59 | return Boolean.TRUE;
60 | }
61 |
62 |
63 | @Override
64 | public EnvVars getEnvironment() throws IOException, InterruptedException {
65 | // call to EnvVars#getRemote like standard Computer does, we get remoting container env, not build container one;
66 | return new EnvVars();
67 | }
68 |
69 | @Override
70 | protected void terminate() {
71 | LOGGER.info("Stopping Docker Slave after build completion");
72 | setAcceptingTasks(false);
73 | super.terminate();
74 | Computer.threadPoolForRemoting.submit(new Runnable() {
75 | @Override
76 | public void run() {
77 | try {
78 | provisioner.clean(getListener());
79 | } catch (IOException | InterruptedException e) {
80 | e.printStackTrace();
81 | }
82 | }
83 | });
84 | }
85 |
86 | public DockerProvisioner getProvisioner() {
87 | return provisioner;
88 | }
89 |
90 | private static final Logger LOGGER = Logger.getLogger(DockerComputer.class.getName());
91 |
92 | public ComputerLauncher createComputerLauncher() {
93 | return new DockerComputerLauncher();
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/ContainersContext.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2015, CloudBees, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | *
24 | */
25 |
26 | package it.dockins.dockerslaves;
27 |
28 | import hudson.model.BuildBadgeAction;
29 | import hudson.model.Run;
30 | import hudson.model.TaskListener;
31 |
32 | import java.util.HashMap;
33 | import java.util.Map;
34 |
35 | public class ContainersContext implements BuildBadgeAction {
36 |
37 | protected String workdirVolume;
38 |
39 | protected Container remotingContainer;
40 |
41 | protected Container buildContainer;
42 |
43 | protected Container scmContainer;
44 |
45 | protected Map sideContainers = new HashMap<>();
46 |
47 | /**
48 | * Flag to indicate the SCM checkout build phase is running.
49 | */
50 | private transient boolean preScm;
51 |
52 | public ContainersContext() {
53 | preScm = true;
54 | }
55 |
56 | public ContainersContext(boolean preScm) {
57 | this.preScm = preScm;
58 | }
59 |
60 | protected void onScmChekoutCompleted(Run, ?> build, TaskListener listener) {
61 | preScm = false;
62 | }
63 |
64 | public String getWorkdirVolume() {
65 | return workdirVolume;
66 | }
67 |
68 | public void setWorkdirVolume(String workdirVolume) {
69 | this.workdirVolume = workdirVolume;
70 | }
71 |
72 | public Container getRemotingContainer() {
73 | return remotingContainer;
74 | }
75 |
76 | public Container getBuildContainer() {
77 | return buildContainer;
78 | }
79 |
80 | public boolean isPreScm() {
81 | return preScm;
82 | }
83 |
84 | public void setRemotingContainer(Container remotingContainer) {
85 | this.remotingContainer = remotingContainer;
86 | }
87 |
88 | public void setBuildContainer(Container buildContainer) {
89 | this.buildContainer = buildContainer;
90 | }
91 |
92 | public Container getScmContainer() {
93 | return scmContainer;
94 | }
95 |
96 | public void setScmContainer(Container scmContainer) {
97 | this.scmContainer = scmContainer;
98 | }
99 |
100 | public Map getSideContainers() {
101 | return sideContainers;
102 | }
103 |
104 | @Override
105 | public String getIconFileName() {
106 | return "/plugin/docker-slaves/images/24x24/docker-logo.png";
107 | }
108 |
109 | @Override
110 | public String getDisplayName() {
111 | return "Docker Build Context";
112 | }
113 |
114 | @Override
115 | public String getUrlName() {
116 | return "docker";
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/pipeline/DockerNodeStep.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright 2016 CloudBees, Inc
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 | package it.dockins.dockerslaves.pipeline;
25 |
26 | import com.google.common.collect.ImmutableSet;
27 | import hudson.EnvVars;
28 | import hudson.Extension;
29 | import hudson.FilePath;
30 | import hudson.Launcher;
31 | import hudson.Util;
32 | import hudson.model.Computer;
33 | import hudson.model.Executor;
34 | import hudson.model.Node;
35 | import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl;
36 | import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl;
37 | import org.kohsuke.stapler.DataBoundConstructor;
38 | import org.kohsuke.stapler.DataBoundSetter;
39 |
40 | import javax.annotation.CheckForNull;
41 | import java.util.List;
42 | import java.util.Set;
43 |
44 | /**
45 | * Provisions a Docker container and run the closure into it like node would do for a normal node
46 | *
47 | *
48 | * Used like:
49 | *
50 | * dockerNode(image:"cloudbees/java-tools", sideContainers: ["selenium/standalone-firefox"]) {
51 | * // execute some stuff inside this container
52 | * }
53 | *
54 | */
55 |
56 | public class DockerNodeStep extends AbstractStepImpl {
57 | private List sideContainers;
58 | private boolean socket = false;
59 |
60 | @CheckForNull
61 | private final String image;
62 |
63 | @DataBoundConstructor
64 | public DockerNodeStep(String image) {
65 | this.image = Util.fixEmptyAndTrim(image);
66 | }
67 |
68 | @CheckForNull
69 | public String getImage() {
70 | return this.image;
71 | }
72 |
73 | public List getSideContainers() {
74 | return sideContainers;
75 | }
76 |
77 | @DataBoundSetter
78 | public void setSideContainers(List sideContainers) {
79 | this.sideContainers = sideContainers;
80 | }
81 |
82 | public boolean getSocket() {
83 | return socket;
84 | }
85 |
86 | @DataBoundSetter
87 | public void setSocket(boolean socket) {
88 | this.socket = socket;
89 | }
90 |
91 | @Extension(optional = true)
92 | public static final class DescriptorImpl extends AbstractStepDescriptorImpl {
93 | public DescriptorImpl() {
94 | super(DockerNodeStepExecution.class);
95 | }
96 |
97 | public String getFunctionName() {
98 | return "dockerNode";
99 | }
100 |
101 | public String getDisplayName() {
102 | return "Allocate a docker node";
103 | }
104 |
105 | public boolean takesImplicitBlockArgument() {
106 | return true;
107 | }
108 |
109 | @SuppressWarnings("unchecked")
110 | @Override
111 | public Set> getProvidedContext() {
112 | return ImmutableSet.of(Executor.class, Computer.class, FilePath.class, EnvVars.class, Node.class, Launcher.class);
113 | }
114 | }
115 | }
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/api/OneShotComputer.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2016, CloudBees, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | *
24 | */
25 | package it.dockins.dockerslaves.api;
26 |
27 | import hudson.Extension;
28 | import hudson.model.Computer;
29 | import hudson.model.Executor;
30 | import hudson.model.Run;
31 | import hudson.model.TaskListener;
32 | import hudson.slaves.ComputerListener;
33 | import hudson.slaves.SlaveComputer;
34 | import jenkins.model.Jenkins;
35 |
36 | import javax.annotation.Nonnull;
37 | import java.io.IOException;
38 | import java.nio.charset.Charset;
39 | import java.nio.charset.StandardCharsets;
40 |
41 | public abstract class OneShotComputer extends SlaveComputer {
42 |
43 | private final OneShotSlave slave;
44 |
45 |
46 | public OneShotComputer(OneShotSlave slave) {
47 | super(slave);
48 | this.slave = slave;
49 | }
50 |
51 | /**
52 | * Claim we are online so we get task assigned to the executor, so a ${@link Run} is created, then can actually
53 | * launch and report provisioning status in the build log.
54 | */
55 | @Override
56 | public boolean isOffline() {
57 | final OneShotSlave node = getNode();
58 | if (node != null) {
59 | if (node.hasProvisioningFailed()) return true;
60 | if (!node.hasExecutable()) return false;
61 | }
62 |
63 | return isActuallyOffline();
64 | }
65 |
66 | public boolean isActuallyOffline() {
67 | return super.isOffline();
68 | }
69 |
70 | @Override
71 | public @Nonnull OneShotSlave getNode() {
72 | return slave;
73 | }
74 |
75 |
76 | @Override
77 | protected void removeExecutor(Executor e) {
78 | terminate();
79 | super.removeExecutor(e);
80 | }
81 |
82 | protected void terminate() {
83 | try {
84 | Jenkins.getInstance().removeNode(slave);
85 | } catch (IOException e) {
86 | e.printStackTrace();
87 | }
88 | }
89 | /**
90 | * We only do support Linux docker images, so we assume UTF-8.
91 | * This let us wait for build log to be created and setup as a
92 | * ${@link hudson.model.BuildListener} before we actually launch
93 | */
94 | @Override
95 | public Charset getDefaultCharset() {
96 | return StandardCharsets.UTF_8;
97 | }
98 |
99 | // --- we need this to workaround hudson.slaves.SlaveComputer#taskListener being private
100 | private TaskListener listener;
101 |
102 | @Extension
103 | public final static ComputerListener COMPUTER_LISTENER = new ComputerListener() {
104 | @Override
105 | public void preLaunch(Computer c, TaskListener listener) throws IOException, InterruptedException {
106 | if (c instanceof OneShotComputer) {
107 | ((OneShotComputer) c).setListener(listener);
108 | }
109 | }
110 | };
111 |
112 | public void setListener(TaskListener listener) {
113 | this.listener = listener;
114 | }
115 |
116 | public TaskListener getListener() {
117 | return listener;
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/spec/DockerfileContainerDefinition.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2015, CloudBees, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | *
24 | */
25 |
26 | package it.dockins.dockerslaves.spec;
27 |
28 | import hudson.Extension;
29 | import hudson.FilePath;
30 | import hudson.Util;
31 | import hudson.model.TaskListener;
32 | import hudson.remoting.VirtualChannel;
33 | import it.dockins.dockerslaves.spi.DockerDriver;
34 | import jenkins.MasterToSlaveFileCallable;
35 | import org.apache.commons.io.FileUtils;
36 | import org.kohsuke.stapler.DataBoundConstructor;
37 |
38 | import java.io.File;
39 | import java.io.IOException;
40 |
41 | /**
42 | * @author Nicolas De Loof
43 | */
44 | public class DockerfileContainerDefinition extends ContainerDefinition {
45 |
46 | private final String dockerfile;
47 |
48 | private final String contextPath;
49 |
50 | private final boolean forcePull;
51 |
52 | private transient String image;
53 |
54 | @DataBoundConstructor
55 | public DockerfileContainerDefinition(String contextPath, String dockerfile, boolean forcePull) {
56 | this.contextPath = contextPath;
57 | this.dockerfile = dockerfile;
58 | this.forcePull = forcePull;
59 | }
60 |
61 | public String getDockerfile() {
62 | return dockerfile;
63 | }
64 |
65 | public String getContextPath() {
66 | return contextPath;
67 | }
68 |
69 | @Override
70 | public String getImage(DockerDriver driver, FilePath workspace, TaskListener listener) throws IOException, InterruptedException {
71 | if (image != null) return image;
72 | String tag = Long.toHexString(System.nanoTime());
73 |
74 | final FilePath pathToContext = workspace.child(contextPath);
75 | if (!pathToContext.exists()) {
76 | throw new IOException(pathToContext.getRemote() + " does not exists.");
77 | }
78 |
79 | final FilePath pathToDockerfile = pathToContext.child(dockerfile);
80 | if (!pathToDockerfile.exists()) {
81 | throw new IOException( pathToContext.getRemote() + " does not exists.");
82 | }
83 |
84 | final File context = Util.createTempDir();
85 | pathToContext.copyRecursiveTo(new FilePath(context));
86 | pathToDockerfile.copyTo(new FilePath(new File(context, "Dockerfile")));
87 |
88 | driver.buildDockerfile(listener, context.getAbsolutePath(), tag, forcePull);
89 | Util.deleteRecursive(context);
90 | this.image = tag;
91 | return tag;
92 | }
93 |
94 | public boolean isForcePull() {
95 | return forcePull;
96 | }
97 |
98 | @Extension
99 | public static class DescriptorImpl extends ContainerDefinitionDescriptor {
100 |
101 | @Override
102 | public String getDisplayName() {
103 | return "Build Dockerfile";
104 | }
105 | }
106 |
107 |
108 | private static MasterToSlaveFileCallable FILECONTENT = new MasterToSlaveFileCallable() {
109 | @Override
110 | public byte[] invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
111 | return FileUtils.readFileToByteArray(f);
112 | }
113 | };
114 | }
115 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/DefaultDockerProvisionerFactory.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2016, CloudBees, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | *
24 | */
25 |
26 | package it.dockins.dockerslaves;
27 |
28 | import hudson.Extension;
29 | import it.dockins.dockerslaves.spi.DockerDriver;
30 | import it.dockins.dockerslaves.spec.ContainerSetDefinition;
31 | import hudson.model.Job;
32 | import hudson.model.Run;
33 | import it.dockins.dockerslaves.spi.DockerDriverFactory;
34 | import it.dockins.dockerslaves.spi.DockerProvisioner;
35 | import it.dockins.dockerslaves.spi.DockerProvisionerFactory;
36 | import it.dockins.dockerslaves.spi.DockerProvisionerFactoryDescriptor;
37 | import org.apache.commons.lang.StringUtils;
38 | import org.kohsuke.stapler.DataBoundConstructor;
39 | import org.kohsuke.stapler.DataBoundSetter;
40 |
41 | import javax.annotation.Nonnull;
42 | import java.io.IOException;
43 |
44 | public class DefaultDockerProvisionerFactory extends DockerProvisionerFactory {
45 |
46 |
47 | private final DockerDriverFactory dockerDriverFactory;
48 |
49 | private String scmImage;
50 |
51 | private String remotingImage;
52 |
53 | @DataBoundConstructor
54 | public DefaultDockerProvisionerFactory(DockerDriverFactory dockerDriverFactory) {
55 | this.dockerDriverFactory = dockerDriverFactory;
56 | }
57 |
58 | public DockerDriverFactory getDockerDriverFactory() {
59 | return dockerDriverFactory;
60 | }
61 |
62 | public String getScmImage() {
63 | return StringUtils.isBlank(scmImage) ? "buildpack-deps:scm" : scmImage;
64 | }
65 |
66 | public String getRemotingImage() {
67 | return StringUtils.isBlank(remotingImage) ? "jenkins/slave" : remotingImage;
68 | }
69 |
70 | @DataBoundSetter
71 | public void setScmImage(String scmImage) {
72 | this.scmImage = scmImage;
73 | }
74 |
75 | @DataBoundSetter
76 | public void setRemotingImage(String remotingImage) {
77 | this.remotingImage = remotingImage;
78 | }
79 |
80 | protected void prepareWorkspace(Job job, ContainersContext context) {
81 |
82 | // TODO define a configurable volume strategy to retrieve a (maybe persistent) workspace
83 |
84 | Run lastBuild = job.getLastCompletedBuild();
85 | if (lastBuild != null) {
86 | ContainersContext previousContext = lastBuild.getAction(ContainersContext.class);
87 | if (previousContext != null && previousContext.getWorkdirVolume() != null) {
88 | context.setWorkdirVolume(previousContext.getWorkdirVolume());
89 | }
90 | }
91 | }
92 |
93 | @Override
94 | public DockerProvisioner createProvisionerForClassicJob(Job job, ContainerSetDefinition spec) throws IOException, InterruptedException {
95 | final DockerDriver driver = dockerDriverFactory.forJob(job);
96 | ContainersContext context = new ContainersContext();
97 | prepareWorkspace(job, context);
98 | return new DefaultDockerProvisioner(context, driver, spec, getRemotingImage(), getScmImage());
99 | }
100 |
101 | @Override
102 | public DockerProvisioner createProvisionerForPipeline(Job job, ContainerSetDefinition spec) throws IOException, InterruptedException {
103 | final DockerDriver driver = dockerDriverFactory.forJob(job);
104 | ContainersContext context = new ContainersContext(false);
105 | return new DefaultDockerProvisioner(context, driver, spec, getRemotingImage(), getScmImage());
106 | }
107 |
108 | @Extension
109 | public static class DescriptorImpl extends DockerProvisionerFactoryDescriptor {
110 |
111 | @Nonnull
112 | @Override
113 | public String getDisplayName() {
114 | return "Compose docker containers";
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/ProvisionQueueListener.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2015, CloudBees, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | *
24 | */
25 |
26 | package it.dockins.dockerslaves;
27 |
28 | import it.dockins.dockerslaves.spec.ContainerSetDefinition;
29 | import hudson.Extension;
30 | import hudson.model.AbstractProject;
31 | import hudson.model.Computer;
32 | import hudson.model.Descriptor;
33 | import hudson.model.Node;
34 | import hudson.model.Queue;
35 | import hudson.model.queue.QueueListener;
36 | import hudson.slaves.Cloud;
37 | import hudson.slaves.NodeProvisioner;
38 | import jenkins.model.Jenkins;
39 |
40 | import java.io.IOException;
41 | import java.util.logging.Level;
42 | import java.util.logging.Logger;
43 |
44 | /**
45 | * {@link Cloud} API is designed to launch virtual machines, which is an heavy process, so relies on
46 | * {@link NodeProvisioner} to determine when a new slave is required. Here we want the slave to start just as a job
47 | * enter the build queue. As an alternative we listen the Queue for Jobs to get scheduled, and when label match
48 | * immediately start a fresh new container executor with a unique label to enforce exclusive usage.
49 | *
50 | */
51 | @Extension
52 | public class ProvisionQueueListener extends QueueListener {
53 |
54 | @Override
55 | public void onEnterBuildable(final Queue.BuildableItem item) {
56 | if (item.task instanceof AbstractProject) {
57 | AbstractProject job = (AbstractProject) item.task;
58 | ContainerSetDefinition def = (ContainerSetDefinition) job.getProperty(ContainerSetDefinition.class);
59 | if (def == null) return;
60 |
61 | try {
62 | final Node node = prepareExecutorFor(item, job);
63 |
64 | DockerSlaveAssignmentAction action = new DockerSlaveAssignmentAction(node.getNodeName());
65 | item.addAction(action);
66 |
67 | Computer.threadPoolForRemoting.submit(new Runnable() {
68 | @Override
69 | public void run() {
70 | try {
71 | Jenkins.getInstance().addNode(node);
72 | } catch (IOException e) {
73 | e.printStackTrace();
74 | }
75 | }
76 | });
77 | } catch (Exception e) {
78 | LOGGER.log(Level.SEVERE, "Failure to create Docker Slave", e);
79 | // TODO: should fail the build here, not just cancel the item without explanation
80 | Jenkins.getInstance().getQueue().cancel(item);
81 | }
82 | }
83 | }
84 |
85 | private Node prepareExecutorFor(final Queue.BuildableItem item, final AbstractProject job) throws Descriptor.FormException, IOException, InterruptedException {
86 | LOGGER.info("Creating a container slave to host " + job.toString() + ", item id " + item.getId());
87 |
88 | // Immediately create a slave for this item
89 | // Real provisioning will happen later
90 | String slaveName = "Container for item " + item.getId();
91 | String description = "Container slave for building " + job.getFullName();
92 | DockerSlaves plugin = DockerSlaves.get();
93 | return new DockerSlave(slaveName, description, null, plugin.createStandardJobProvisionerFactory(job),item);
94 | }
95 |
96 | /**
97 | * If item is canceled, remove the executor we created for it.
98 | */
99 | @Override
100 | public void onLeft(Queue.LeftItem item) {
101 | if (item.isCancelled()) {
102 | DockerSlaveAssignmentAction action = item.getAction(DockerSlaveAssignmentAction.class);
103 | if( action == null) return;
104 | Node slave = action.getAssignedNodeName();
105 | if (slave == null) return;
106 | try {
107 | Jenkins.getInstance().removeNode(slave);
108 | } catch (Exception e) {
109 | LOGGER.log(Level.SEVERE, "Failure to remove One-Shot Slave", e);
110 | }
111 | }
112 | }
113 |
114 | private static final Logger LOGGER = Logger.getLogger(ProvisionQueueListener.class.getName());
115 | }
116 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/DockerSlave.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2015, CloudBees, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | *
24 | */
25 |
26 | package it.dockins.dockerslaves;
27 |
28 | import hudson.Extension;
29 | import hudson.Launcher;
30 | import hudson.model.Computer;
31 | import hudson.model.Descriptor;
32 | import hudson.model.Job;
33 | import hudson.model.Queue;
34 | import hudson.model.Run;
35 | import hudson.model.TaskListener;
36 | import hudson.model.listeners.RunListener;
37 | import hudson.model.listeners.SCMListener;
38 | import hudson.remoting.Channel;
39 | import hudson.scm.ChangeLogSet;
40 | import hudson.scm.SCM;
41 | import hudson.slaves.EphemeralNode;
42 | import it.dockins.dockerslaves.api.OneShotSlave;
43 | import it.dockins.dockerslaves.spi.DockerProvisioner;
44 |
45 | import java.io.IOException;
46 |
47 | /**
48 | * An ${@link EphemeralNode} using docker containers to host the build processes.
49 | * Slave is dedicated to a specific ${@link Job}, and even better to a specific build, but when this class
50 | * is created the build does not yet exists due to Jenkins lifecycle.
51 | */
52 | public class DockerSlave extends OneShotSlave {
53 |
54 | public static final String SLAVE_ROOT = "/home/jenkins/";
55 |
56 | private final DockerProvisioner provisioner;
57 |
58 | private final long queueItemId;
59 |
60 | public DockerSlave(String name, String nodeDescription, String labelString, DockerProvisioner provisioner, Queue.Item queueItem) throws Descriptor.FormException, IOException {
61 | // TODO would be better to get notified when the build start, and get the actual build ID. But can't find the API for that
62 | super(name.replaceAll("/", " » "), nodeDescription, SLAVE_ROOT, labelString, new DockerComputerLauncher());
63 | this.provisioner = provisioner;
64 | this.queueItemId = queueItem.getId();
65 | }
66 |
67 | public DockerComputer createComputer() {
68 | return new DockerComputer(this, provisioner);
69 | }
70 |
71 | @Override
72 | public DockerSlave asNode() {
73 | return this;
74 | }
75 |
76 | @Override
77 | public DockerComputer getComputer() {
78 | return (DockerComputer) super.getComputer();
79 | }
80 |
81 | public long getQueueItemId() {
82 | return queueItemId;
83 | }
84 |
85 | /**
86 | * Create a custom ${@link Launcher} which relies on docker run to start a new process
87 | */
88 | @Override
89 | public Launcher createLauncher(TaskListener listener) {
90 | DockerComputer c = getComputer();
91 | if (c == null) {
92 | listener.error("Issue with creating launcher for slave " + name + ".");
93 | throw new IllegalStateException("Can't create a launcher if computer is gone.");
94 | }
95 |
96 | super.createLauncher(listener);
97 | final Channel channel = c.getChannel();
98 | if (channel == null) throw new IllegalStateException("Can't create a Launcher: channel not connected");
99 | return new DockerLauncher(listener, channel, c.isUnix(), provisioner)
100 | .decorateFor(this);
101 | }
102 |
103 | /**
104 | * This listener get notified as the build is going to start.
105 | */
106 | @Extension
107 | public static class DockerSlaveRunListener extends RunListener {
108 |
109 | @Override
110 | public void onStarted(Run run, TaskListener listener) {
111 | Computer c = Computer.currentComputer();
112 | if (c instanceof DockerComputer) {
113 | run.addAction(((DockerComputer) c).getProvisioner().getContext());
114 | }
115 | }
116 | }
117 |
118 | /**
119 | * This listener get notified as the build completes the SCM checkout. We use this event to determine when the
120 | * build has to switch from SCM docker images to Build images to host build steps execution.
121 | */
122 | @Extension
123 | public static class DockerSlaveSCMListener extends SCMListener {
124 | @Override
125 | public void onChangeLogParsed(Run, ?> build, SCM scm, TaskListener listener, ChangeLogSet> changelog) throws Exception {
126 | final ContainersContext action = build.getAction(ContainersContext.class);
127 | if (action != null) {
128 | action.onScmChekoutCompleted(build, listener);
129 | }
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/DockerSlaves.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2015, CloudBees, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | *
24 | */
25 |
26 | package it.dockins.dockerslaves;
27 |
28 | import hudson.Extension;
29 | import hudson.Plugin;
30 | import hudson.model.Describable;
31 | import hudson.model.Descriptor;
32 | import hudson.model.Job;
33 | import hudson.slaves.Cloud;
34 | import it.dockins.dockerslaves.drivers.PlainDockerAPIDockerDriverFactory;
35 | import it.dockins.dockerslaves.spec.ContainerSetDefinition;
36 | import it.dockins.dockerslaves.spi.DockerProvisioner;
37 | import it.dockins.dockerslaves.spi.DockerProvisionerFactory;
38 | import jenkins.model.Jenkins;
39 | import net.sf.json.JSONObject;
40 | import org.jenkinsci.plugins.docker.commons.credentials.DockerServerEndpoint;
41 | import org.kohsuke.stapler.DataBoundSetter;
42 | import org.kohsuke.stapler.StaplerRequest;
43 |
44 | import javax.servlet.ServletException;
45 | import java.io.IOException;
46 |
47 | /**
48 | * {@link Cloud} implementation designed to launch a set of containers (aka "pod") to establish a Jenkins executor.
49 | *
50 | */
51 | public class DockerSlaves extends Plugin implements Describable {
52 |
53 | /**
54 | * Base Build image name. Build commands will run on it.
55 | */
56 | private String defaultBuildContainerImageName;
57 |
58 | private DockerProvisionerFactory dockerProvisionerFactory;
59 |
60 | private int maxSlaves = 10;
61 |
62 | public void start() throws IOException {
63 | load();
64 | }
65 |
66 | @Override
67 | public void configure(StaplerRequest req, JSONObject formData) throws IOException, ServletException, Descriptor.FormException {
68 | req.bindJSON(this, formData);
69 | save();
70 | }
71 |
72 | public String getDefaultBuildContainerImageName() {
73 | return defaultBuildContainerImageName;
74 | }
75 |
76 | @DataBoundSetter
77 | public void setDefaultBuildContainerImageName(String defaultBuildContainerImageName) {
78 | this.defaultBuildContainerImageName = defaultBuildContainerImageName;
79 | }
80 |
81 | @DataBoundSetter
82 | public void setDockerProvisionerFactory(DockerProvisionerFactory dockerProvisionerFactory) {
83 | this.dockerProvisionerFactory = dockerProvisionerFactory;
84 | }
85 |
86 | public DockerProvisionerFactory getDockerProvisionerFactory() {
87 | if (dockerProvisionerFactory == null) {
88 | final DefaultDockerProvisionerFactory factory = new DefaultDockerProvisionerFactory(
89 | new PlainDockerAPIDockerDriverFactory(dockerHost));
90 | factory.setRemotingImage(remotingContainerImageName);
91 | factory.setScmImage(scmContainerImageName);
92 | return dockerProvisionerFactory = factory;
93 | }
94 | return dockerProvisionerFactory;
95 | }
96 |
97 | public DockerProvisioner createStandardJobProvisionerFactory(Job job) throws IOException, InterruptedException {
98 | // TODO iterate on job's ItemGroup and it's parents so end-user can configure this at folder level.
99 |
100 | ContainerSetDefinition spec = (ContainerSetDefinition) job.getProperty(ContainerSetDefinition.class);
101 | return getDockerProvisionerFactory().createProvisionerForClassicJob(job, spec);
102 | }
103 |
104 | public int getMaxSlaves() {
105 | return maxSlaves;
106 | }
107 |
108 | @DataBoundSetter
109 | public void setMaxSlaves(int maxSlaves) {
110 | this.maxSlaves = maxSlaves;
111 | }
112 |
113 | public DockerProvisioner createProvisionerForPipeline(Job job, ContainerSetDefinition spec) throws IOException, InterruptedException {
114 | return getDockerProvisionerFactory().createProvisionerForPipeline(job, spec);
115 | }
116 |
117 | public static DockerSlaves get() {
118 | return Jenkins.getInstance().getPlugin(DockerSlaves.class);
119 | }
120 |
121 | @Override
122 | public Descriptor getDescriptor() {
123 | return Jenkins.getInstance().getDescriptorOrDie(DockerSlaves.class);
124 | }
125 |
126 |
127 | static {
128 | Jenkins.XSTREAM.aliasPackage("xyz.quoidneufdocker.jenkins", "it.dockins");
129 | }
130 |
131 | @Extension
132 | public static class DescriptorImpl extends Descriptor {
133 |
134 | @Override
135 | public String getDisplayName() {
136 | return "Docker Slaves";
137 | }
138 | }
139 |
140 |
141 | /// --- kept for backward compatibility
142 |
143 | public String scmContainerImageName;
144 |
145 | public String remotingContainerImageName;
146 |
147 | public DockerServerEndpoint dockerHost;
148 | }
149 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/spec/ContainerSetDefinition.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2015, CloudBees, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | *
24 | */
25 |
26 | package it.dockins.dockerslaves.spec;
27 |
28 | import com.google.common.base.Predicate;
29 | import com.google.common.collect.Collections2;
30 | import hudson.Extension;
31 | import hudson.model.AbstractProject;
32 | import hudson.model.Job;
33 | import hudson.model.JobProperty;
34 | import hudson.model.JobPropertyDescriptor;
35 | import it.dockins.dockerslaves.DockerSlaves;
36 | import it.dockins.dockerslaves.spi.DockerProvisionerFactory;
37 | import net.sf.json.JSONObject;
38 | import org.kohsuke.stapler.DataBoundConstructor;
39 | import org.kohsuke.stapler.StaplerRequest;
40 |
41 | import javax.annotation.Nullable;
42 | import java.util.Collection;
43 | import java.util.Collections;
44 | import java.util.List;
45 |
46 |
47 |
48 | /**
49 | * Definition for a set of containers to host the build.
50 | * @author Nicolas De Loof
51 | */
52 | public class ContainerSetDefinition extends JobProperty {
53 |
54 | private final ContainerDefinition buildHostImage;
55 |
56 | private final List sideContainers;
57 |
58 | @DataBoundConstructor
59 | public ContainerSetDefinition(ContainerDefinition buildHostImage, List sideContainers) {
60 | this.buildHostImage = buildHostImage;
61 | this.sideContainers = sideContainers == null ? Collections.emptyList() : sideContainers;
62 | }
63 |
64 | /**
65 | * When deserializing the config.xml file, XStream will instantiate a JobBuildsContainersDefinition
66 | * without going through the constructor; this means that any checks or default values that might
67 | * have been written in said constructor will be bypassed.
68 | *
69 | * Fortunately, XStream calls the readResolve before the deserialized object
70 | * is returned to its parent. We simply recreate a JobBuildsContainersDefinition using the
71 | * deserialized values to replace the original one.
72 | *
73 | * @return a replacement JobBuildsContainersDefinition that went through the constructor
74 | */
75 | private Object readResolve() {
76 | return new ContainerSetDefinition(buildHostImage, sideContainers);
77 | }
78 |
79 | public ContainerDefinition getBuildHostImage() {
80 | return buildHostImage;
81 | // return StringUtils.isBlank(buildHostImage) ? DockerSlaves.get().getDefaultBuildContainerImageName() : buildHostImage;
82 | }
83 |
84 | public List getSideContainers() {
85 | return sideContainers;
86 | }
87 |
88 | @Extension
89 | public static class DescriptorImpl extends JobPropertyDescriptor {
90 |
91 | @Override
92 | public boolean isApplicable(Class extends Job> type) {
93 | return AbstractProject.class.isAssignableFrom(type);
94 | }
95 |
96 | @Override
97 | public String getDisplayName() {
98 | return "Containers to host the build";
99 | }
100 |
101 | @Override
102 | public JobProperty> newInstance(StaplerRequest req, JSONObject formData) throws FormException {
103 | if (formData.isNullObject()) return null;
104 | JSONObject containersDefinition = formData.getJSONObject("containersDefinition");
105 | if (containersDefinition.isNullObject()) return null;
106 | return req.bindJSON(ContainerSetDefinition.class, containersDefinition);
107 | }
108 |
109 | public Collection getMainContainerDescriptors() {
110 | final DockerProvisionerFactory factory = DockerSlaves.get().getDockerProvisionerFactory();
111 |
112 | return Collections2.filter(ContainerDefinitionDescriptor.all(), new Predicate() {
113 | @Override
114 | public boolean apply(@Nullable ContainerDefinitionDescriptor d) {
115 | return factory.canBeUsedAsMainContainer(d);
116 | }
117 | });
118 | }
119 |
120 | public Collection getSideContainerDescriptors() {
121 | final DockerProvisionerFactory factory = DockerSlaves.get().getDockerProvisionerFactory();
122 |
123 | return Collections2.filter(ContainerDefinitionDescriptor.all(), new Predicate() {
124 | @Override
125 | public boolean apply(@Nullable ContainerDefinitionDescriptor d) {
126 | return factory.canBeUsedAsSideContainer(d);
127 | }
128 | });
129 | }
130 |
131 | }
132 |
133 | }
134 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
28 | 4.0.0
29 |
30 |
31 | org.jenkins-ci.plugins
32 | plugin
33 | 2.33
34 |
35 |
36 | io.jenkins.plugins
37 | docker-slaves
38 | 1.0.8-SNAPSHOT
39 | hpi
40 |
41 | Docker Slaves Plugin
42 | Uses Docker containers to run Jenkins build agents
43 | https://wiki.jenkins-ci.org/display/JENKINS/Docker+Slaves+Plugin
44 |
45 |
46 | MIT License
47 | http://opensource.org/licenses/MIT
48 |
49 |
50 |
51 |
52 |
53 | ndeloof
54 | Nicolas De Loof
55 | nicolas.deloof@gmail.com
56 |
57 |
58 | ydubreuil
59 | Yoann Dubreuil
60 | yoann.dubreuil@gmail.com
61 |
62 |
63 |
64 |
65 | scm:git:git://github.com/jenkinsci/docker-slaves-plugin.git
66 | scm:git:git@github.com:jenkinsci/docker-slaves-plugin.git
67 | https://github.com/jenkinsci/docker-slaves-plugin
68 | HEAD
69 |
70 |
71 |
72 | 2.19.3
73 | false
74 |
75 |
76 |
77 |
78 | org.jenkins-ci.plugins.workflow
79 | workflow-step-api
80 | 2.3
81 | true
82 |
83 |
84 | org.jenkins-ci.plugins.workflow
85 | workflow-support
86 | 2.2
87 | true
88 |
89 |
90 | org.jenkins-ci.plugins
91 | durable-task
92 | 1.12
93 | true
94 |
95 |
96 | org.jenkins-ci.plugins
97 | docker-commons
98 | 1.3.1
99 |
100 |
101 | com.google.code.findbugs
102 | annotations
103 | 3.0.1
104 | provided
105 |
106 |
107 |
108 |
109 |
110 | repo.jenkins-ci.org
111 | https://repo.jenkins-ci.org/public/
112 |
113 |
114 |
115 |
116 | repo.jenkins-ci.org
117 | https://repo.jenkins-ci.org/public/
118 |
119 |
120 |
121 |
122 |
123 |
124 | run-with-plugins
125 |
126 |
127 | org.jenkins-ci.plugins.workflow
128 | workflow-aggregator
129 | 2.3
130 |
131 |
132 | org.jenkins-ci.plugins
133 | git
134 | 2.6.0
135 |
136 |
137 | org.jenkins-ci.plugins
138 | credentials
139 | 2.1.4
140 |
141 |
142 |
143 |
144 |
145 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/DefaultDockerProvisioner.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2015, CloudBees, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | *
24 | */
25 |
26 | package it.dockins.dockerslaves;
27 |
28 | import it.dockins.dockerslaves.spec.ContainerDefinition;
29 | import it.dockins.dockerslaves.spec.Hint;
30 | import it.dockins.dockerslaves.spi.DockerDriver;
31 | import it.dockins.dockerslaves.spec.ContainerSetDefinition;
32 | import hudson.Launcher;
33 | import hudson.Proc;
34 | import hudson.model.TaskListener;
35 | import it.dockins.dockerslaves.spec.SideContainerDefinition;
36 | import it.dockins.dockerslaves.spi.DockerProvisioner;
37 |
38 | import java.io.IOException;
39 | import java.util.Collections;
40 |
41 | /**
42 | * Provision {@link Container}s based on ${@link ContainerSetDefinition} to provide a queued task
43 | * an executor.
44 | */
45 | public class DefaultDockerProvisioner extends DockerProvisioner {
46 |
47 | protected final ContainersContext context;
48 |
49 | protected final DockerDriver driver;
50 |
51 | protected final ContainerSetDefinition spec;
52 |
53 | protected final String remotingImage;
54 |
55 | protected final String scmImage;
56 |
57 |
58 | public DefaultDockerProvisioner(ContainersContext context, DockerDriver driver, ContainerSetDefinition spec, String remotingImage, String scmImage) throws IOException, InterruptedException {
59 | this.context = context;
60 | this.driver = driver;
61 | this.spec = spec;
62 | this.remotingImage = remotingImage;
63 | this.scmImage = scmImage;
64 | }
65 |
66 | @Override
67 | public ContainersContext getContext() {
68 | return context;
69 | }
70 |
71 | @Override
72 | public Container launchRemotingContainer(final DockerComputer computer, TaskListener listener) throws IOException, InterruptedException {
73 | // if remoting container already exists, we reuse it
74 | final Container existing = context.getRemotingContainer();
75 | if (existing != null) {
76 | if (driver.hasContainer(listener, existing.getId())) {
77 | return existing;
78 | }
79 | }
80 |
81 | String volume = context.getWorkdirVolume();
82 | if (!driver.hasVolume(listener, volume)) {
83 | volume = driver.createVolume(listener);
84 | context.setWorkdirVolume(volume);
85 | }
86 |
87 | final Container remotingContainer = driver.launchRemotingContainer(listener, remotingImage, volume, computer);
88 | context.setRemotingContainer(remotingContainer);
89 | return remotingContainer;
90 | }
91 |
92 | @Override
93 | public Container launchBuildContainers(Launcher.ProcStarter starter, TaskListener listener) throws IOException, InterruptedException {
94 | if (spec.getSideContainers().size() > 0 && context.getSideContainers().size() == 0) {
95 | // In a ideal world we would run side containers when DockerSlave.DockerSlaveSCMListener detect scm checkout completed
96 | // but then we don't have a ProcStarter reference. So do it first time a command is ran during the build
97 | // after scm checkout completed. We detect this is the first time as spec > context
98 | createSideContainers(starter, listener);
99 | }
100 |
101 | final ContainerDefinition build = spec.getBuildHostImage();
102 | String buildImage = build.getImage(driver, starter.pwd(), listener);
103 | final Container buildContainer = driver.launchBuildContainer(listener, buildImage, context.getRemotingContainer(), build.getHints());
104 | context.setBuildContainer(buildContainer);
105 | return buildContainer;
106 | }
107 |
108 | @Override
109 | public Container launchScmContainer(TaskListener listener) throws IOException, InterruptedException {
110 | final Container scmContainer = driver.launchBuildContainer(listener, scmImage, context.getRemotingContainer(), Collections.emptyList());
111 | context.setBuildContainer(scmContainer);
112 | return scmContainer;
113 | }
114 |
115 | private void createSideContainers(Launcher.ProcStarter starter, TaskListener listener) throws IOException, InterruptedException {
116 | for (SideContainerDefinition definition : spec.getSideContainers()) {
117 | final String name = definition.getName();
118 | final ContainerDefinition sidecar = definition.getSpec();
119 | final String image = sidecar.getImage(driver, starter.pwd(), listener);
120 | listener.getLogger().println("Starting " + name + " container");
121 | Container container = driver.launchSideContainer(listener, image, context.getRemotingContainer(), sidecar.getHints());
122 | context.getSideContainers().put(name, container);
123 | }
124 | }
125 |
126 | @Override
127 | public Proc launchBuildProcess(Launcher.ProcStarter procStarter, TaskListener listener) throws IOException, InterruptedException {
128 | Container targetContainer = null;
129 |
130 | if (context.isPreScm()) {
131 | targetContainer = context.getScmContainer();
132 | if (targetContainer == null) {
133 | targetContainer = launchScmContainer(listener);
134 | }
135 | } else {
136 | targetContainer = context.getBuildContainer();
137 | if (targetContainer == null) {
138 | targetContainer = launchBuildContainers(procStarter, listener);
139 | }
140 | }
141 | return driver.execInContainer(listener, targetContainer.getId(), procStarter);
142 | }
143 |
144 | @Override
145 | public void clean(TaskListener listener) throws IOException, InterruptedException {
146 | for (Container instance : context.getSideContainers().values()) {
147 | driver.removeContainer(listener, instance);
148 | }
149 |
150 | if (context.getBuildContainer() != null) {
151 | driver.removeContainer(listener, context.getBuildContainer());
152 | }
153 |
154 | if (context.getScmContainer() != null) {
155 | driver.removeContainer(listener, context.getScmContainer());
156 | }
157 |
158 | driver.close();
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Docker Slaves Plugin
2 |
3 | This plugin allows to execute a jenkins job inside a (set of) container(s).
4 | On Job configuration page, an option let you define a set of containers to host your build and provide test resources
5 | (database, webserver, whatever). There's no constraint on the image to use, as all the jenkins related plumbing is
6 | handled transparently by the plugin.
7 |
8 | 
9 |
10 | See [announcement](http://blog.loof.fr/2015/09/introducing-docker-slaves-jenkins-plugin.html) and [demo](https://www.youtube.com/watch?v=HbwgN0UTTxo)
11 |
12 | ## Status
13 |
14 | Prototyping (Docker Global Hack Day). Don't run in production. Use at your own risk. etc.
15 | Current implementation focus on Docker runtime, after cleanup the docker specific code will be isolated in a dedicated docker-slaves plugin.
16 |
17 | see https://issues.jenkins-ci.org/browse/JENKINS/component/20839 for issues/tasks/RFE
18 |
19 | ### What works?
20 |
21 | The following things have been tested and works
22 |
23 | * Freestyle job
24 | * Maven job (as long as you configure a Maven installation with an automatic installer and you have a JDK in the build container)
25 | * Pipeline job
26 | * Timestamper plugin
27 | * Git plugin
28 |
29 | You can find sources of demos here: https://github.com/ydubreuil/docker-slaves-plugin-demos
30 |
31 | ### Pipeline job support
32 |
33 | There's an experimental support for Pipeline plugin. The idea is to replace `node` with `dockerNode`.
34 |
35 | This pipeline
36 |
37 | ```groovy
38 | dockerNode(image: "maven:3.3.3-jdk-8", sideContainers: ["selenium/standalone-firefox"]) {
39 | git "https://github.com/wakaleo/game-of-life"
40 | sh 'mvn clean test'
41 | }
42 | ```
43 |
44 | will build Game of life with Maven 3.3.3 on JDK 8 on a disposable Docker pod. Tests use Firefox browser provided by a standalone Selenium driver hosted in a side container.
45 |
46 | Workspace caching does not work.
47 |
48 | For discussion around the implementation, see [this document](Workflow.md)
49 |
50 | ### Swarm support
51 |
52 | As of Docker 1.10 and Swarm 1.1, some early tests showed that using a Swarm cluster works, ie builds are working. It means that Docker API used by the plugin works on Swarm. There's no dedicated code to manage Swarm.
53 |
54 | ## General Design
55 |
56 | Global configuration let administrator setup the container infrastructure. Typically, a DockerHost URL, but could be extended by third party plugin to connect to another container hosting service, for sample to adapt to Kubernetes Pod API or Rkt container engine. Just need to be [opencontainer](https://www.opencontainers.org/) compliant.
57 |
58 | To host a build, plugin will :
59 | * create a data container to host the project workspace.
60 | * run a predefined slave container which is designed to just establish jenkins remoting channel.
61 | * run a container for the scm to checkout project code
62 | * (optionally) build a fresh new container image based on a Dockerfile stored in SCM
63 | * run a (set of) containers configured by user as part of the job configuration. All them are linked together and share network
64 |
65 | ## Architecture
66 |
67 | Plugin do rely on Jenkins Cloud API. Global configuration do only define a label, as slave template is actually declared in job configuration as a NodeProperty.
68 | This property allow user to define a container image to host the build, and an optional set of additional images to link to this one - those can be used to host a test database, or comparable resources. Need to consider if we could rely on docker-compose.yml syntax.
69 | Internally, a unique slave image is defined and is responsible to establish jenkins remoting.
70 |
71 | When a job is triggered, job configuration + remoting image do define a container group ("pod") the plugin has to run. ContainerProvisionner is responsible to run this pod.
72 |
73 | also see [Architecture.md](Architecture.md)
74 |
75 | ## [Docker](https://www.docker.com) implementation
76 |
77 | Plugin includes a ContainerProvisionner implementation based on Docker CLI. This one will move to docker-slaves plugin when we get a reasonable design and can isolate general container
78 | API.
79 |
80 | This implementation do run the slave remoting container using a plain `docker run` command and rely on docker stdin/stdout as remoting transport (i.e. CommandLauncher or equivalent).
81 | The Launcher is decorated so command/process to be launched on the slave are directly executed with `docker exec`.
82 |
83 | General idea is to avoid to use Jenkins remoting to launch processes but directly rely on Docker for this (what docker finally is is just an `execve` on steroids!). That magically brings long-running tasks for free.
84 |
85 | 
86 |
87 | Also Read [Docker implementation](Docker.md)
88 |
89 | Note: this implementation relies on docker cli ran from jenkins master, and as such is using threads to manage the transient slave stdin/stdout steams. A NIO version would be lot's more efficient.
90 |
91 |
92 | # Future
93 |
94 | ## Provisioning issue reporting
95 |
96 | As the build container(s) are only used by a build, we'd like the container bootstrap log to be included in the job logs, or at least attached to the build action. This would help to diagnose provisioning issues.
97 | For the same purpose, when the initial remoting container can't be provisioned, we'd like to mark the build as `NOT_BUILT` and attach docker logs
98 |
99 | ## Alternate implementations
100 | Plugin is designed on top of Docker CLI features, but the general concept could apply to other container engines / docker cluster managers. We plan to extract a common skeleton into container-slaves-plugin, and experiment with alternate implementations.
101 |
102 | ### Kubernetes implementation
103 |
104 | Kubernetes has native support for Pod concept, so would embrace this design with minimal effort.
105 | Data container would rely on a [kubernetes volume](https://github.com/kubernetes/kubernetes/blob/master/docs/user-guide/volumes.md)
106 |
107 | ### Amazon ECS implementation
108 |
109 | Comparable to Kubernetes.
110 |
111 | ### Mesos implementation
112 |
113 | To be considered
114 |
115 | ### [Rkt](https://github.com/coreos/rkt) implementation
116 |
117 | Supporting rkt runtime could be great from a security POV. rkt is able to launch containers isolated inside a small KVM process, greatly enhancing security (https://coreos.com/blog/rkt-0.8-with-new-vm-support/)
118 |
119 | ## Other ideas
120 | * Browse workspace after build completion by running a fresh new container with volumes-from build's data-container
121 | * Slave view do offer a terminal access to the slave environment. Could rely on https://wiki.jenkins-ci.org/display/JENKINS/Remote+Terminal+Access+Plugin
122 | * Side containers or build container as a axis in multi-configuration job
123 | * Build throttling
124 | * Memory High Water Mark monitoring
125 | * Integrate with ClearContainers for enhanced security
126 |
127 | ### Scalability
128 | * Introduce and extension point to get Dockerhost based on job to run. Can rely on docker-swarm with container affinity, can also be a set of hosts managed by jenkins, running a dedicated monitoring container to check host load, optionally auto-scaling (using docker-machine ?).
129 |
130 | ### Perf enhancements
131 | * Put remoting JAR cache into docker slave image so launching slave will be much faster (add a second, read-only cache directory in `hudson.remoting.FileSystemJarCache`). On startup jenkins would then build the remoting image, then remoting channel can start without delay for future builds.
132 |
133 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/api/OneShotSlave.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2016, CloudBees, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | *
24 | */
25 | package it.dockins.dockerslaves.api;
26 |
27 | import hudson.Extension;
28 | import hudson.Launcher;
29 | import hudson.model.Computer;
30 | import hudson.model.Descriptor;
31 | import hudson.model.Executor;
32 | import hudson.model.Queue;
33 | import hudson.model.Result;
34 | import hudson.model.Run;
35 | import hudson.model.Slave;
36 | import hudson.model.TaskListener;
37 | import hudson.model.listeners.RunListener;
38 | import hudson.slaves.ComputerLauncher;
39 | import hudson.slaves.EphemeralNode;
40 | import hudson.slaves.NodeProperty;
41 | import hudson.slaves.RetentionStrategy;
42 | import hudson.slaves.SlaveComputer;
43 | import jenkins.model.Jenkins;
44 |
45 | import java.io.IOException;
46 | import java.util.Collections;
47 |
48 | /**
49 | * A slave that is designed to be used only once, for a specific ${@link hudson.model.Run}, and as such has a life cycle
50 | * to fully match the Run's one.
51 | *
52 | * Provisioning such a slave should be a lightweight process, so one can provision them at any time and concurrently
53 | * to match ${@link hudson.model.Queue} load. Typical usage is Docker container based Jenkins agents.
54 | *
55 | * Actual launch of the Slave is postponed until a ${@link Run} is created, so we can have a 1:1 match between Run and
56 | * Executor lifecycle:
57 | *
58 | * - dump the launch log in build log.
59 | * - mark the build as ${@link Result#NOT_BUILT} on launch failure.
60 | * - shut down and remove the Executor on build completion
61 | *
62 | */
63 | public abstract class OneShotSlave extends Slave implements EphemeralNode {
64 |
65 | /**
66 | * The ${@link Queue.Executable} associated to this OneShotSlave. By design, only one Run can be assigned, then slave is shut down.
67 | * This field is set as soon as the ${@link Queue.Executable} has been created.
68 | */
69 | private transient Queue.Executable executable;
70 |
71 | private final ComputerLauncher realLauncher;
72 |
73 | private boolean provisioningFailed = false;
74 |
75 | public OneShotSlave(String name, String nodeDescription, String remoteFS, String labelString, ComputerLauncher launcher) throws Descriptor.FormException, IOException {
76 | super(name, nodeDescription, remoteFS, 1, Mode.EXCLUSIVE, labelString, NOOP_LAUNCHER, RetentionStrategy.NOOP, Collections.>emptyList());
77 | this.realLauncher = launcher;
78 | }
79 |
80 | @Override
81 | public int getNumExecutors() {
82 | return 1;
83 | }
84 |
85 |
86 | /*package*/ boolean hasExecutable() {
87 | return executable != null;
88 | }
89 |
90 | /*package*/ boolean hasProvisioningFailed() {
91 | return provisioningFailed;
92 | }
93 |
94 | @Override
95 | public OneShotComputer getComputer() {
96 | return (OneShotComputer) super.getComputer();
97 | }
98 |
99 |
100 |
101 |
102 | /**
103 | * Assign a ${@link Queue.Executable} to this OneShotSlave. By design, only one Queue.Executable can be assigned, then slave is shut down.
104 | * This method has to be called just as the ${@link Run} as been created. It run the actual launch of the executor
105 | * and use Run's ${@link hudson.model.BuildListener} as computer launcher listener to collect the startup log as part of the build.
106 | *
107 | * Delaying launch of the executor until the Run is actually started allows to fail the build on launch failure,
108 | * so we have a strong 1:1 relation between a Run and it's Executor.
109 | * @param listener
110 | */
111 | synchronized void provision(TaskListener listener) {
112 | if (executable != null) {
113 | // already provisioned
114 | return;
115 | }
116 |
117 | final Executor executor = Executor.currentExecutor();
118 | if (executor == null) {
119 | throw new IllegalStateException("running task without associated executor thread");
120 | }
121 |
122 | try {
123 | realLauncher.launch(this.getComputer(), listener);
124 |
125 | if (getComputer().isActuallyOffline()) {
126 | provisionFailed(new IllegalStateException("Computer is offline after launch"));
127 | }
128 | } catch (Exception e) {
129 | provisionFailed(e);
130 | }
131 | executable = executor.getCurrentExecutable();
132 | }
133 |
134 | void provisionFailed(Exception cause) {
135 | if (executable instanceof Run) {
136 | ((Run) executable).setResult(Result.NOT_BUILT);
137 | }
138 |
139 | try {
140 | Jenkins.getInstance().removeNode(this);
141 | } catch (IOException e) {
142 | e.printStackTrace();
143 | }
144 |
145 | throw new OneShotExecutorProvisioningException(cause);
146 | }
147 |
148 | /**
149 | * Pipeline does not use the same mechanism to use nodes, so we also need to consider ${@link #createLauncher(TaskListener)}
150 | * as an event to determine first use of the slave.
151 | */
152 | @Override
153 | public Launcher createLauncher(TaskListener listener) {
154 | provision(listener);
155 | return super.createLauncher(listener);
156 | }
157 |
158 | /**
159 | * We listen to loggers creation by ${@link Run}s so we can write the executor's launch log into build log.
160 | * Relying on this implementation detail is fragile, but we don't really have a better
161 | * option yet.
162 | */
163 | @Extension(ordinal = Double.MAX_VALUE)
164 | public final static RunListener RUN_LISTENER = new RunListener() {
165 | @Override
166 | public void onStarted(Run run, TaskListener listener) {
167 | Computer c = Computer.currentComputer();
168 | if (c instanceof OneShotComputer) {
169 | final OneShotSlave node = ((OneShotComputer) c).getNode();
170 | node.provision(listener);
171 | }
172 | }
173 | };
174 |
175 | private static final ComputerLauncher NOOP_LAUNCHER = new ComputerLauncher() {
176 | @Override
177 | public void launch(SlaveComputer computer, TaskListener listener) throws IOException, InterruptedException {
178 | //noop;
179 | }
180 | };
181 | }
182 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/drivers/CliDockerDriver.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2015, CloudBees, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | *
24 | */
25 |
26 | package it.dockins.dockerslaves.drivers;
27 |
28 | import hudson.Launcher;
29 | import hudson.Proc;
30 | import hudson.model.Slave;
31 | import hudson.model.TaskListener;
32 | import hudson.slaves.CommandLauncher;
33 | import hudson.util.ArgumentListBuilder;
34 | import hudson.util.VersionNumber;
35 | import it.dockins.dockerslaves.Container;
36 | import it.dockins.dockerslaves.DockerComputer;
37 | import it.dockins.dockerslaves.DockerSlave;
38 | import it.dockins.dockerslaves.ProvisionQueueListener;
39 | import it.dockins.dockerslaves.hints.MemoryHint;
40 | import it.dockins.dockerslaves.hints.VolumeHint;
41 | import it.dockins.dockerslaves.spec.Hint;
42 | import it.dockins.dockerslaves.spi.DockerDriver;
43 | import it.dockins.dockerslaves.spi.DockerHostConfig;
44 | import org.apache.commons.io.IOUtils;
45 | import org.apache.commons.lang.StringUtils;
46 | import org.apache.tools.tar.TarEntry;
47 | import org.apache.tools.tar.TarInputStream;
48 | import org.apache.tools.tar.TarOutputStream;
49 | import org.jenkinsci.plugins.docker.commons.credentials.DockerServerEndpoint;
50 |
51 | import java.io.ByteArrayInputStream;
52 | import java.io.ByteArrayOutputStream;
53 | import java.io.IOException;
54 | import java.io.OutputStream;
55 | import java.nio.charset.StandardCharsets;
56 | import java.util.List;
57 | import java.util.logging.Level;
58 | import java.util.logging.Logger;
59 |
60 | import static it.dockins.dockerslaves.DockerSlave.SLAVE_ROOT;
61 |
62 | /**
63 | * @author Nicolas De Loof
64 | * @author Yoann Dubreuil
65 | */
66 | public class CliDockerDriver extends DockerDriver {
67 |
68 | private final static boolean verbose = Boolean.getBoolean(DockerDriver.class.getName()+".verbose");
69 |
70 | private final DockerHostConfig dockerHost;
71 |
72 | private final VersionNumber version;
73 |
74 | public CliDockerDriver(DockerHostConfig dockerHost) throws IOException, InterruptedException {
75 | this.dockerHost = dockerHost;
76 | // Also acts as sanity check to ensure host and credentials are well set
77 | version = new VersionNumber(serverVersion(TaskListener.NULL));
78 | }
79 |
80 | @Override
81 | public void close() throws IOException {
82 | dockerHost.close();
83 | }
84 |
85 | @Override
86 | public String createVolume(TaskListener listener) throws IOException, InterruptedException {
87 | ArgumentListBuilder args = new ArgumentListBuilder()
88 | .add("volume", "create");
89 |
90 | ByteArrayOutputStream out = new ByteArrayOutputStream();
91 | Launcher launcher = new Launcher.LocalLauncher(listener);
92 | int status = launchDockerCLI(launcher, args)
93 | .stdout(out).stderr(launcher.getListener().getLogger()).join();
94 |
95 | final String volume = out.toString(UTF_8).trim();
96 |
97 | if (status != 0) {
98 | throw new IOException("Failed to create docker volume");
99 | }
100 |
101 | return volume;
102 | }
103 |
104 | @Override
105 | public boolean hasVolume(TaskListener listener, String name) throws IOException, InterruptedException {
106 | if (StringUtils.isEmpty(name)) {
107 | return false;
108 | }
109 |
110 | ArgumentListBuilder args = new ArgumentListBuilder()
111 | .add("volume", "inspect", "-f", "'{{.Name}}'", name);
112 |
113 | ByteArrayOutputStream out = new ByteArrayOutputStream();
114 | Launcher launcher = new Launcher.LocalLauncher(listener);
115 | int status = launchDockerCLI(launcher, args)
116 | .stdout(out).stderr(launcher.getListener().getLogger()).join();
117 |
118 | return status == 0;
119 | }
120 |
121 |
122 | @Override
123 | public boolean hasContainer(TaskListener listener, String id) throws IOException, InterruptedException {
124 | if (StringUtils.isEmpty(id)) {
125 | return false;
126 | }
127 |
128 | ArgumentListBuilder args = new ArgumentListBuilder()
129 | .add("inspect", "-f", "'{{.Id}}'", id);
130 |
131 | ByteArrayOutputStream out = new ByteArrayOutputStream();
132 | Launcher launcher = new Launcher.LocalLauncher(listener);
133 | int status = launchDockerCLI(launcher, args)
134 | .stdout(out).stderr(launcher.getListener().getLogger()).join();
135 |
136 | return status == 0;
137 | }
138 |
139 | @Override
140 | public Container launchRemotingContainer(TaskListener listener, String image, String volume, DockerComputer computer) throws IOException, InterruptedException {
141 |
142 | // Create a container for remoting
143 | ArgumentListBuilder args = new ArgumentListBuilder()
144 | .add("create", "--interactive")
145 |
146 | // We disable container logging to sdout as we rely on this one as transport for jenkins remoting
147 | .add("--log-driver=none")
148 |
149 | .add("--env", "TMPDIR="+ SLAVE_ROOT+".tmp")
150 | .add("--user", "10000:10000")
151 | .add("--rm")
152 | .add("--volume", volume+":"+ SLAVE_ROOT)
153 | .add(image)
154 | .add("java")
155 | // set TMP directory within the /home/jenkins/ volume so it can be shared with other containers
156 | .add("-Djava.io.tmpdir="+ SLAVE_ROOT+".tmp")
157 | .add("-jar").add(SLAVE_ROOT+"slave.jar");
158 |
159 | ByteArrayOutputStream out = new ByteArrayOutputStream();
160 | Launcher launcher = new Launcher.LocalLauncher(listener);
161 | int status = launchDockerCLI(launcher, args)
162 | .stdout(out).stderr(launcher.getListener().getLogger()).join();
163 |
164 | String containerId = out.toString(UTF_8).trim();
165 |
166 | if (status != 0) {
167 | throw new IOException("Failed to create docker image");
168 | }
169 |
170 | // Inject current slave.jar to ensure adequate version running
171 | putFileContent(launcher, containerId, DockerSlave.SLAVE_ROOT, "slave.jar", new Slave.JnlpJar("slave.jar").readFully());
172 | Container remotingContainer = new Container(image, containerId);
173 |
174 | // Run container in interactive mode to establish channel over stdin/stdout
175 | args = new ArgumentListBuilder()
176 | .add("start")
177 | .add("--interactive", "--attach", remotingContainer.getId());
178 | prependArgs(args);
179 | new CommandLauncher(args.toString(), dockerHost.getEnvironment()).launch(computer, listener);
180 | return remotingContainer;
181 | }
182 |
183 | @Override
184 | public Container launchBuildContainer(TaskListener listener, String image, Container remotingContainer, List hints) throws IOException, InterruptedException {
185 | Container buildContainer = new Container(image);
186 | ArgumentListBuilder args = new ArgumentListBuilder()
187 | .add("create")
188 | .add("--env", "TMPDIR="+SLAVE_ROOT+".tmp")
189 | .add("--workdir", SLAVE_ROOT)
190 | .add("--volumes-from", remotingContainer.getId())
191 | .add("--net=container:" + remotingContainer.getId())
192 | .add("--ipc=container:" + remotingContainer.getId())
193 | .add("--user", "10000:10000");
194 |
195 | applyHints(hints, args);
196 |
197 | args.add(buildContainer.getImageName());
198 |
199 | args.add("/trampoline", "wait");
200 |
201 | ByteArrayOutputStream out = new ByteArrayOutputStream();
202 | Launcher launcher = new Launcher.LocalLauncher(listener);
203 | int status = launchDockerCLI(launcher, args)
204 | .stdout(out).stderr(launcher.getListener().getLogger()).join();
205 |
206 | final String containerId = out.toString(UTF_8).trim();
207 | buildContainer.setId(containerId);
208 |
209 | if (status != 0) {
210 | throw new IOException("Failed to run docker image");
211 | }
212 |
213 | injectJenkinsUnixGroup(launcher, containerId);
214 | injectJenkinsUnixUser(launcher, containerId);
215 | injectTrampoline(launcher, containerId);
216 |
217 | status = launchDockerCLI(launcher, new ArgumentListBuilder()
218 | .add("start", containerId)).stdout(out).stderr(launcher.getListener().getLogger()).join();
219 |
220 | if (status != 0) {
221 | throw new IOException("Failed to run docker image");
222 | }
223 |
224 | return buildContainer;
225 | }
226 |
227 | private void applyHints(List hints, ArgumentListBuilder args) {
228 | if (hints == null) return;
229 | for (Hint hint : hints) {
230 | if (hint instanceof MemoryHint) {
231 | args.add("-m", ((MemoryHint) hint).getMemory());
232 | } else if (hint instanceof VolumeHint) {
233 | args.add("-v", ((VolumeHint) hint).getVolume());
234 | } else {
235 | // unsupported hint, just ignored
236 | }
237 | }
238 | }
239 |
240 | protected void injectJenkinsUnixGroup(Launcher launcher, String containerId) throws IOException, InterruptedException {
241 | ByteArrayOutputStream out = new ByteArrayOutputStream();
242 | getFileContent(launcher, containerId, "/etc/group", out);
243 | out.write("jenkins:x:10000:\n".getBytes(StandardCharsets.UTF_8));
244 | putFileContent(launcher, containerId, "/etc", "group", out.toByteArray());
245 | }
246 |
247 | protected void injectJenkinsUnixUser(Launcher launcher, String containerId) throws IOException, InterruptedException {
248 | ByteArrayOutputStream out = new ByteArrayOutputStream();
249 | getFileContent(launcher, containerId, "/etc/passwd", out);
250 | out.write("jenkins:x:10000:10000::/home/jenkins:/bin/false\n".getBytes(StandardCharsets.UTF_8));
251 | putFileContent(launcher, containerId, "/etc", "passwd", out.toByteArray());
252 | }
253 |
254 | protected void injectTrampoline(Launcher launcher, String containerId) throws IOException, InterruptedException {
255 | ByteArrayOutputStream out = new ByteArrayOutputStream();
256 | IOUtils.copy(getClass().getResourceAsStream("/it/dockins/dockerslaves/trampoline"),out);
257 | putFileContent(launcher, containerId, "/", "trampoline", out.toByteArray(), 555);
258 | }
259 |
260 | protected void getFileContent(Launcher launcher, String containerId, String filename, OutputStream outputStream) throws IOException, InterruptedException {
261 | ArgumentListBuilder args = new ArgumentListBuilder()
262 | .add("cp", containerId + ":" + filename, "-");
263 |
264 | ByteArrayOutputStream out = new ByteArrayOutputStream();
265 | int status = launchDockerCLI(launcher, args)
266 | .stdout(out).stderr(launcher.getListener().getLogger()).join();
267 |
268 | if (status != 0) {
269 | throw new IOException("Failed to get file");
270 | }
271 |
272 | TarInputStream tar = new TarInputStream(new ByteArrayInputStream(out.toByteArray()));
273 | tar.getNextEntry();
274 | tar.copyEntryContents(outputStream);
275 | tar.close();
276 | }
277 |
278 | protected int putFileContent(Launcher launcher, String containerId, String path, String filename, byte[] content) throws IOException, InterruptedException {
279 | return putFileContent(launcher, containerId, path, filename, content, null);
280 | }
281 |
282 | protected int putFileContent(Launcher launcher, String containerId, String path, String filename, byte[] content, Integer mode) throws IOException, InterruptedException {
283 | TarEntry entry = new TarEntry(filename);
284 | entry.setUserId(0);
285 | entry.setGroupId(0);
286 | entry.setSize(content.length);
287 | if (mode != null) {
288 | entry.setMode(mode);
289 | }
290 |
291 | ByteArrayOutputStream out = new ByteArrayOutputStream();
292 | TarOutputStream tar = new TarOutputStream(out);
293 | tar.putNextEntry(entry);
294 | tar.write(content);
295 | tar.closeEntry();
296 | tar.close();
297 |
298 | ArgumentListBuilder args = new ArgumentListBuilder()
299 | .add("cp", "-", containerId + ":" + path);
300 |
301 | return launchDockerCLI(launcher, args)
302 | .stdin(new ByteArrayInputStream(out.toByteArray()))
303 | .stderr(launcher.getListener().getLogger()).join();
304 | }
305 |
306 | @Override
307 | public Proc execInContainer(TaskListener listener, String containerId, Launcher.ProcStarter starter) throws IOException, InterruptedException {
308 | ArgumentListBuilder args = new ArgumentListBuilder()
309 | .add("exec", containerId);
310 |
311 | if (starter.pwd() != null) {
312 | args.add("/trampoline", "cdexec", starter.pwd().getRemote());
313 | }
314 |
315 | args.add("env").add(starter.envs());
316 |
317 | List originalCmds = starter.cmds();
318 | boolean[] originalMask = starter.masks();
319 | for (int i = 0; i < originalCmds.size(); i++) {
320 | boolean masked = originalMask == null ? false : i < originalMask.length ? originalMask[i] : false;
321 | args.add(originalCmds.get(i), masked);
322 | }
323 |
324 | Launcher launcher = new Launcher.LocalLauncher(listener);
325 | Launcher.ProcStarter procStarter = launchDockerCLI(launcher, args);
326 |
327 | if (starter.stdout() != null) {
328 | procStarter.stdout(starter.stdout());
329 | }
330 |
331 | return procStarter.start();
332 | }
333 |
334 | @Override
335 | public void removeContainer(TaskListener listener, Container instance) throws IOException, InterruptedException {
336 | ArgumentListBuilder args = new ArgumentListBuilder()
337 | .add("rm", "-f", instance.getId());
338 |
339 | ByteArrayOutputStream out = new ByteArrayOutputStream();
340 | Launcher launcher = new Launcher.LocalLauncher(listener);
341 | int status = launchDockerCLI(launcher, args)
342 | .stdout(out).stderr(launcher.getListener().getLogger()).join();
343 |
344 | if (status != 0) {
345 | throw new IOException("Failed to remove container " + instance.getId());
346 | }
347 | }
348 |
349 | private static final Logger LOGGER = Logger.getLogger(ProvisionQueueListener.class.getName());
350 |
351 | @Override
352 | public Container launchSideContainer(TaskListener listener, String image, Container remotingContainer, List hints) throws IOException, InterruptedException {
353 | ArgumentListBuilder args = new ArgumentListBuilder()
354 | .add("create")
355 | .add("--volumes-from", remotingContainer.getId())
356 | .add("--net=container:" + remotingContainer.getId())
357 | .add("--ipc=container:" + remotingContainer.getId());
358 |
359 | applyHints(hints, args);
360 |
361 | args.add(image);
362 |
363 | ByteArrayOutputStream out = new ByteArrayOutputStream();
364 | Launcher launcher = new Launcher.LocalLauncher(listener);
365 | int status = launchDockerCLI(launcher, args)
366 | .stdout(out).stderr(launcher.getListener().getLogger()).join();
367 |
368 | final String containerId = out.toString(UTF_8).trim();
369 |
370 |
371 | if (status != 0) {
372 | throw new IOException("Failed to run docker image");
373 | }
374 |
375 | launchDockerCLI(launcher, new ArgumentListBuilder()
376 | .add("start", containerId)).start();
377 |
378 | return new Container(image, containerId);
379 | }
380 |
381 | @Override
382 | public void pullImage(TaskListener listener, String image) throws IOException, InterruptedException {
383 | ArgumentListBuilder args = new ArgumentListBuilder()
384 | .add("pull")
385 | .add(image);
386 |
387 | Launcher launcher = new Launcher.LocalLauncher(listener);
388 | int status = launchDockerCLI(launcher, args)
389 | .stdout(launcher.getListener().getLogger()).join();
390 |
391 | if (status != 0) {
392 | throw new IOException("Failed to pull image " + image);
393 | }
394 | }
395 |
396 | @Override
397 | public boolean checkImageExists(TaskListener listener, String image) throws IOException, InterruptedException {
398 | ArgumentListBuilder args = new ArgumentListBuilder()
399 | .add("inspect")
400 | .add("-f", "'{{.Id}}'")
401 | .add(image);
402 |
403 | ByteArrayOutputStream out = new ByteArrayOutputStream();
404 | Launcher launcher = new Launcher.LocalLauncher(listener);
405 | return launchDockerCLI(launcher, args)
406 | .stdout(out).stderr(launcher.getListener().getLogger()).join() == 0;
407 | }
408 |
409 | @Override
410 | public void buildDockerfile(TaskListener listener, String dockerfilePath, String tag, boolean pull) throws IOException, InterruptedException {
411 | String pullOption = "--pull=";
412 | if (pull) {
413 | pullOption += "true";
414 | } else {
415 | pullOption += "false";
416 | }
417 | ArgumentListBuilder args = new ArgumentListBuilder()
418 | .add("build")
419 | .add(pullOption)
420 | .add("-t", tag)
421 | .add(dockerfilePath);
422 |
423 | Launcher launcher = new Launcher.LocalLauncher(listener);
424 | int status = launchDockerCLI(launcher, args)
425 | .stdout(launcher.getListener().getLogger()).join();
426 |
427 | if (status != 0) {
428 | throw new IOException("Failed to build docker image from Dockerfile " + dockerfilePath);
429 | }
430 | }
431 |
432 | @Override
433 | public String serverVersion(TaskListener listener) throws IOException, InterruptedException {
434 | ArgumentListBuilder args = new ArgumentListBuilder()
435 | .add("version", "-f", "{{.Server.Version}}");
436 |
437 | ByteArrayOutputStream out = new ByteArrayOutputStream();
438 | Launcher launcher = new Launcher.LocalLauncher(listener);
439 | int status = launchDockerCLI(launcher, args)
440 | .stdout(out).stderr(launcher.getListener().getLogger()).join();
441 |
442 | final String version = out.toString(UTF_8).trim();
443 |
444 | if (status != 0) {
445 | throw new IOException("Failed to connect to docker API");
446 | }
447 |
448 | return version;
449 | }
450 |
451 | VersionNumber SWARM = new VersionNumber("1.12");
452 | VersionNumber INFO_FORMAT = new VersionNumber("1.13");
453 |
454 | public boolean usesSwarmMode(TaskListener listener) throws IOException, InterruptedException {
455 | if (version.isOlderThan(SWARM)) return false;
456 |
457 | ArgumentListBuilder args = new ArgumentListBuilder()
458 | .add("docker", "info");
459 | if (!version.isOlderThan(INFO_FORMAT)) {
460 | args.add("--format", "{{.Swarm}}");
461 | }
462 |
463 | ByteArrayOutputStream out = new ByteArrayOutputStream();
464 | Launcher launcher = new Launcher.LocalLauncher(listener);
465 | int status = launchDockerCLI(launcher, args)
466 | .stdout(out).stderr(launcher.getListener().getLogger()).join();
467 |
468 | if (status != 0) {
469 | throw new IOException("Failed to connect to docker API");
470 | }
471 |
472 | return out.toString(UTF_8).contains("Swarm: active");
473 | }
474 |
475 | public void prependArgs(ArgumentListBuilder args){
476 | final DockerServerEndpoint endpoint = dockerHost.getEndpoint();
477 | if (endpoint.getUri() != null) {
478 | args.prepend("-H", endpoint.getUri());
479 | } else {
480 | LOGGER.log(Level.FINE, "no specified docker host");
481 | }
482 |
483 | args.prepend("docker");
484 | }
485 |
486 | private Launcher.ProcStarter launchDockerCLI(Launcher launcher, ArgumentListBuilder args) {
487 | prependArgs(args);
488 |
489 | return launcher.launch()
490 | .envs(dockerHost.getEnvironment())
491 | .cmds(args)
492 | .quiet(!verbose);
493 | }
494 |
495 | public static final String UTF_8 = StandardCharsets.UTF_8.name();
496 | }
497 |
--------------------------------------------------------------------------------
/src/main/java/it/dockins/dockerslaves/pipeline/DockerNodeStepExecution.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright 2016 CloudBees, Inc
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 | package it.dockins.dockerslaves.pipeline;
25 |
26 | import hudson.AbortException;
27 | import hudson.Extension;
28 | import hudson.model.queue.QueueListener;
29 | import it.dockins.dockerslaves.DockerSlave;
30 | import it.dockins.dockerslaves.DockerSlaves;
31 | import it.dockins.dockerslaves.spec.ContainerSetDefinition;
32 | import it.dockins.dockerslaves.spec.DockerSocketContainerDefinition;
33 | import it.dockins.dockerslaves.spec.ImageIdContainerDefinition;
34 | import it.dockins.dockerslaves.spec.SideContainerDefinition;
35 | import com.google.inject.Inject;
36 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
37 | import hudson.EnvVars;
38 | import hudson.FilePath;
39 | import hudson.Launcher;
40 | import hudson.model.Computer;
41 | import hudson.model.Executor;
42 | import hudson.model.Item;
43 | import hudson.model.Job;
44 | import hudson.model.Label;
45 | import hudson.model.Node;
46 | import hudson.model.Queue;
47 | import hudson.model.ResourceList;
48 | import hudson.model.Run;
49 | import hudson.model.TaskListener;
50 | import hudson.model.TopLevelItem;
51 | import hudson.model.queue.CauseOfBlockage;
52 | import hudson.model.queue.SubTask;
53 | import hudson.remoting.ChannelClosedException;
54 | import hudson.remoting.RequestAbortedException;
55 | import hudson.security.ACL;
56 | import hudson.security.AccessControlled;
57 | import hudson.security.Permission;
58 | import hudson.slaves.WorkspaceList;
59 | import jenkins.model.Jenkins;
60 | import jenkins.model.queue.AsynchronousExecution;
61 | import jenkins.util.Timer;
62 | import org.acegisecurity.AccessDeniedException;
63 | import org.acegisecurity.Authentication;
64 | import org.jenkinsci.plugins.durabletask.executors.ContinuableExecutable;
65 | import org.jenkinsci.plugins.durabletask.executors.ContinuedTask;
66 | import org.jenkinsci.plugins.workflow.flow.FlowExecution;
67 | import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner;
68 | import org.jenkinsci.plugins.workflow.graph.FlowNode;
69 | import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl;
70 | import org.jenkinsci.plugins.workflow.steps.BodyExecutionCallback;
71 | import org.jenkinsci.plugins.workflow.steps.StepContext;
72 | import org.jenkinsci.plugins.workflow.steps.StepContextParameter;
73 | import org.jenkinsci.plugins.workflow.support.actions.WorkspaceActionImpl;
74 | import org.kohsuke.accmod.Restricted;
75 | import org.kohsuke.accmod.restrictions.DoNotUse;
76 | import org.kohsuke.accmod.restrictions.NoExternalUse;
77 | import org.kohsuke.stapler.export.ExportedBean;
78 |
79 | import javax.annotation.CheckForNull;
80 | import javax.annotation.Nullable;
81 | import java.io.IOException;
82 | import java.io.PrintStream;
83 | import java.io.Serializable;
84 | import java.util.ArrayList;
85 | import java.util.Collection;
86 | import java.util.Collections;
87 | import java.util.HashMap;
88 | import java.util.List;
89 | import java.util.Map;
90 | import java.util.UUID;
91 | import java.util.concurrent.TimeUnit;
92 | import java.util.logging.Level;
93 | import java.util.logging.Logger;
94 |
95 | import static java.util.logging.Level.FINE;
96 | import static java.util.logging.Level.WARNING;
97 |
98 | /**
99 | * This class is heavily based (so to speak...) on org.jenkinsci.plugins.workflow.support.steps.ExecutorStepExecution
100 | * The original code cannot be easily extended, PlaceholderTask constructor is private for example.
101 | *
102 | * As much as we hate copy / pasting, as an experimental implementation, we feel that it is still interesting
103 | * to test the approach.
104 | *
105 | * See https://github.com/jenkinsci/workflow-durable-task-step-plugin/pull/15 for a discussion about that.
106 | */
107 | public class DockerNodeStepExecution extends AbstractStepExecutionImpl {
108 |
109 | @Inject(optional = true)
110 | private transient DockerNodeStep step;
111 | @StepContextParameter
112 | private transient TaskListener listener;
113 | @StepContextParameter
114 | private transient Run, ?> run;
115 | // Here just for requiredContext; could perhaps be passed to the PlaceholderTask constructor:
116 | @StepContextParameter
117 | private transient FlowExecution flowExecution;
118 | @StepContextParameter
119 | private transient FlowNode flowNode;
120 |
121 | /*
122 | * General strategy of this step.
123 | *
124 | * 1. schedule {@link PlaceholderTask} into the {@link Queue} (what this method does)
125 | * 2. when {@link PlaceholderTask} starts running, invoke the closure
126 | * 3. when the closure is done, let {@link PlaceholderTask} complete
127 | */
128 | @Override
129 | public boolean start() throws Exception {
130 | final String label = "docker_" + Long.toHexString(System.nanoTime());
131 | final PlaceholderTask task = new PlaceholderTask(getContext(), label, run);
132 |
133 | final DockerSlaves cloud = DockerSlaves.get();
134 |
135 | String slaveName = "Container for " + run.toString() + "." + flowNode.getId();
136 | String description = "Container building " + run.getParent().getFullName();
137 |
138 | Queue.Item item = Queue.getInstance().schedule2(task, 0).getCreateItem();
139 | if (item == null) {
140 | // There can be no duplicates. But could be refused if a QueueDecisionHandler rejects it for some odd reason.
141 | throw new IllegalStateException("failed to schedule task");
142 | }
143 |
144 | List sideContainers = new ArrayList<>();
145 | if (step.getSideContainers() != null) {
146 | for (String entry : step.getSideContainers()) {
147 | sideContainers.add(new SideContainerDefinition(entry,
148 | new ImageIdContainerDefinition(entry, false)));
149 | }
150 | }
151 | if(step.getSocket()) {
152 | sideContainers.add(new SideContainerDefinition("socket", new DockerSocketContainerDefinition()));
153 | }
154 |
155 | ContainerSetDefinition spec = new ContainerSetDefinition(
156 | new ImageIdContainerDefinition(step.getImage(), false), sideContainers);
157 |
158 | final Node node = new DockerSlave(slaveName, description, label,
159 | cloud.createProvisionerForPipeline(run.getParent(), spec), item);
160 |
161 | Jenkins.getInstance().addNode(node);
162 |
163 | Timer.get().schedule(new Runnable() {
164 | @Override public void run() {
165 | Queue.Item item = Queue.getInstance().getItem(task);
166 | if (item != null) {
167 | PrintStream logger;
168 | try {
169 | logger = listener.getLogger();
170 | } catch (Exception x) { // IOException, InterruptedException
171 | LOGGER.log(WARNING, null, x);
172 | return;
173 | }
174 | logger.println("Still waiting to schedule task");
175 | String why = item.getWhy();
176 | if (why != null) {
177 | logger.println(why);
178 | }
179 | }
180 | }
181 | }, 15, TimeUnit.SECONDS);
182 | return false;
183 | }
184 |
185 | @Override
186 | public void stop(Throwable cause) {
187 | for (Queue.Item item : Queue.getInstance().getItems()) {
188 | // if we are still in the queue waiting to be scheduled, just retract that
189 | if (item.task instanceof PlaceholderTask && ((PlaceholderTask) item.task).context.equals(getContext())) {
190 | Queue.getInstance().cancel(item);
191 | break;
192 | }
193 | }
194 | Jenkins j = Jenkins.getInstance();
195 | if (j != null) {
196 | // if we are already running, kill the ongoing activities, which releases PlaceholderExecutable from its sleep loop
197 | // Similar to Executor.of, but distinct since we do not have the Executable yet:
198 | COMPUTERS: for (Computer c : j.getComputers()) {
199 | for (Executor e : c.getExecutors()) {
200 | Queue.Executable exec = e.getCurrentExecutable();
201 | if (exec instanceof PlaceholderTask.PlaceholderExecutable && ((PlaceholderTask.PlaceholderExecutable) exec).getParent().context.equals(getContext())) {
202 | PlaceholderTask.finish(((PlaceholderTask.PlaceholderExecutable) exec).getParent().cookie);
203 | break COMPUTERS;
204 | }
205 | }
206 | }
207 | }
208 | // Whether or not either of the above worked (and they would not if for example our item were canceled), make sure we die.
209 | getContext().onFailure(cause);
210 | }
211 |
212 | @Override public void onResume() {
213 | super.onResume();
214 | // See if we are still running, or scheduled to run. Cf. stop logic above.
215 | for (Queue.Item item : Queue.getInstance().getItems()) {
216 | if (item.task instanceof PlaceholderTask && ((PlaceholderTask) item.task).context.equals(getContext())) {
217 | LOGGER.log(FINE, "Queue item for node block in {0} is still waiting after reload", run);
218 | return;
219 | }
220 | }
221 | Jenkins j = Jenkins.getInstance();
222 | if (j != null) {
223 | COMPUTERS: for (Computer c : j.getComputers()) {
224 | for (Executor e : c.getExecutors()) {
225 | Queue.Executable exec = e.getCurrentExecutable();
226 | if (exec instanceof PlaceholderTask.PlaceholderExecutable && ((PlaceholderTask.PlaceholderExecutable) exec).getParent().context.equals(getContext())) {
227 | LOGGER.log(FINE, "Node block in {0} is running on {1} after reload", new Object[] {run, c.getName()});
228 | return;
229 | }
230 | }
231 | }
232 | }
233 | if (step == null) { // compatibility: used to be transient
234 | listener.getLogger().println("Queue item for node block in " + run.getFullDisplayName() + " is missing (perhaps JENKINS-34281), but cannot reschedule");
235 | return;
236 | }
237 | listener.getLogger().println("Queue item for node block in " + run.getFullDisplayName() + " is missing (perhaps JENKINS-34281); rescheduling");
238 | try {
239 | start();
240 | } catch (Exception x) {
241 | getContext().onFailure(x);
242 | }
243 | }
244 |
245 | @Override public String getStatus() {
246 | // Yet another copy of the same logic; perhaps this should be factored into some method returning a union of Queue.Item and PlaceholderExecutable?
247 | for (Queue.Item item : Queue.getInstance().getItems()) {
248 | if (item.task instanceof PlaceholderTask && ((PlaceholderTask) item.task).context.equals(getContext())) {
249 | return "waiting for " + item.task.getFullDisplayName() + " to be scheduled; blocked: " + item.getWhy();
250 | }
251 | }
252 | Jenkins j = Jenkins.getInstance();
253 | if (j != null) {
254 | COMPUTERS: for (Computer c : j.getComputers()) {
255 | for (Executor e : c.getExecutors()) {
256 | Queue.Executable exec = e.getCurrentExecutable();
257 | if (exec instanceof PlaceholderTask.PlaceholderExecutable && ((PlaceholderTask.PlaceholderExecutable) exec).getParent().context.equals(getContext())) {
258 | return "running on " + c.getName();
259 | }
260 | }
261 | }
262 | }
263 | return "node block appears to be neither running nor scheduled";
264 | }
265 |
266 | @Extension public static class CancelledItemListener extends QueueListener {
267 |
268 | @Override public void onLeft(Queue.LeftItem li) {
269 | if (li.isCancelled()) {
270 | if (li.task instanceof PlaceholderTask) {
271 | (((PlaceholderTask) li.task).context).onFailure(new AbortException(Messages.DockerNodeStepExecution_queue_task_cancelled()));
272 | }
273 | }
274 | }
275 |
276 | }
277 |
278 | /** Transient handle of a running executor task. */
279 | private static final class RunningTask {
280 | /** null until placeholder executable runs */
281 | @Nullable AsynchronousExecution execution;
282 | /** null until placeholder executable runs */
283 | @Nullable Launcher launcher;
284 | }
285 |
286 | private static final String COOKIE_VAR = "JENKINS_SERVER_COOKIE";
287 |
288 | @ExportedBean
289 | public static final class PlaceholderTask implements ContinuedTask, Serializable, AccessControlled {
290 |
291 | /** keys are {@link #cookie}s */
292 | private static final Map runningTasks = new HashMap<>();
293 |
294 | private final StepContext context;
295 | private String label;
296 | /** Shortcut for {@link #run}. */
297 | private String runId;
298 | /**
299 | * Unique cookie set once the task starts.
300 | * Serves multiple purposes:
301 | * identifies whether we have already invoked the body (since this can be rerun after restart);
302 | * serves as a key for {@link #runningTasks} and {@link Callback} (cannot just have a doneness flag in {@link PlaceholderTask} because multiple copies might be deserialized);
303 | * and allows {@link Launcher#kill} to work.
304 | */
305 | private String cookie;
306 |
307 | PlaceholderTask(StepContext context, String label, Run,?> run) {
308 | this.context = context;
309 | this.label = label;
310 | runId = run.getExternalizableId();
311 | }
312 |
313 | private Object readResolve() {
314 | LOGGER.log(FINE, "deserialized {0}", cookie);
315 | if (cookie != null) {
316 | synchronized (runningTasks) {
317 | runningTasks.put(cookie, new RunningTask());
318 | }
319 | }
320 | return this;
321 | }
322 |
323 | @Override public Queue.Executable createExecutable() throws IOException {
324 | return new PlaceholderExecutable();
325 | }
326 |
327 | @Override public Label getAssignedLabel() {
328 | if (label == null) {
329 | return null;
330 | } else if (label.isEmpty()) {
331 | Jenkins j = Jenkins.getInstance();
332 | if (j == null) {
333 | return null;
334 | }
335 | return j.getSelfLabel();
336 | } else {
337 | return Label.get(label);
338 | }
339 | }
340 |
341 | @Override public Node getLastBuiltOn() {
342 | if (label == null) {
343 | return null;
344 | }
345 | Jenkins j = Jenkins.getInstance();
346 | if (j == null) {
347 | return null;
348 | }
349 | return j.getNode(label);
350 | }
351 |
352 | @Override public boolean isBuildBlocked() {
353 | return false;
354 | }
355 |
356 | @Deprecated
357 | @Override public String getWhyBlocked() {
358 | return null;
359 | }
360 |
361 | @Override public CauseOfBlockage getCauseOfBlockage() {
362 | return null;
363 | }
364 |
365 | @Override public boolean isConcurrentBuild() {
366 | return false;
367 | }
368 |
369 | @Override public Collection extends SubTask> getSubTasks() {
370 | return Collections.singleton(this);
371 | }
372 |
373 | @Override public Queue.Task getOwnerTask() {
374 | Run,?> r = run();
375 | if (r != null && r.getParent() instanceof Queue.Task) {
376 | return (Queue.Task) r.getParent();
377 | } else {
378 | return this;
379 | }
380 | }
381 |
382 | @Override public Object getSameNodeConstraint() {
383 | return null;
384 | }
385 |
386 | /**
387 | * Something we can use to check abort and read permissions.
388 | * Normally this will be a {@link Run}.
389 | * However if things are badly broken, for example if the build has been deleted,
390 | * then as a fallback we use the Jenkins root.
391 | * This allows an administrator to clean up dead queue items and executor cells.
392 | * TODO make {@link FlowExecutionOwner} implement {@link AccessControlled}
393 | * so that an implementation could fall back to checking {@link Job} permission.
394 | */
395 | @Override public ACL getACL() {
396 | try {
397 | if (!context.isReady()) {
398 | return Jenkins.getInstance().getACL();
399 | }
400 | FlowExecution exec = context.get(FlowExecution.class);
401 | if (exec == null) {
402 | return Jenkins.getInstance().getACL();
403 | }
404 | Queue.Executable executable = exec.getOwner().getExecutable();
405 | if (executable instanceof AccessControlled) {
406 | return ((AccessControlled) executable).getACL();
407 | } else {
408 | return Jenkins.getInstance().getACL();
409 | }
410 | } catch (Exception x) {
411 | LOGGER.log(FINE, null, x);
412 | return Jenkins.getInstance().getACL();
413 | }
414 | }
415 |
416 | @Override public void checkPermission(Permission p) throws AccessDeniedException {
417 | getACL().checkPermission(p);
418 | }
419 |
420 | @Override public boolean hasPermission(Permission p) {
421 | return getACL().hasPermission(p);
422 | }
423 |
424 | @Override public void checkAbortPermission() {
425 | checkPermission(Item.CANCEL);
426 | }
427 |
428 | @Override public boolean hasAbortPermission() {
429 | return hasPermission(Item.CANCEL);
430 | }
431 |
432 | public @CheckForNull Run,?> run() {
433 | try {
434 | if (!context.isReady()) {
435 | return null;
436 | }
437 | return context.get(Run.class);
438 | } catch (Exception x) {
439 | LOGGER.log(FINE, "broken " + cookie, x);
440 | finish(cookie); // probably broken, so just shut it down
441 | return null;
442 | }
443 | }
444 |
445 | public @CheckForNull Run,?> runForDisplay() {
446 | Run,?> r = run();
447 | if (r == null && /* not stored prior to 1.13 */runId != null) {
448 | return Run.fromExternalizableId(runId);
449 | }
450 | return r;
451 | }
452 |
453 | @Override public String getUrl() {
454 | // TODO ideally this would be found via FlowExecution.owner.executable, but how do we check for something with a URL? There is no marker interface for it: JENKINS-26091
455 | Run,?> r = runForDisplay();
456 | return r != null ? r.getUrl() : "";
457 | }
458 |
459 | @Override public String getDisplayName() {
460 | // TODO more generic to check whether FlowExecution.owner.executable is a ModelObject
461 | Run,?> r = runForDisplay();
462 | return r != null ? Messages.DockerNodeStepExecution_PlaceholderTask_displayName(r.getFullDisplayName()) : Messages.DockerNodeStepExecution_PlaceholderTask_displayName_unknown();
463 | }
464 |
465 | @Override public String getName() {
466 | return getDisplayName();
467 | }
468 |
469 | @Override public String getFullDisplayName() {
470 | return getDisplayName();
471 | }
472 |
473 | @Override public long getEstimatedDuration() {
474 | Run,?> r = run();
475 | // Not accurate if there are multiple slaves in one build, but better than nothing:
476 | return r != null ? r.getEstimatedDuration() : -1;
477 | }
478 |
479 | @Override public ResourceList getResourceList() {
480 | return new ResourceList();
481 | }
482 |
483 | @Override public Authentication getDefaultAuthentication() {
484 | return ACL.SYSTEM; // TODO should pick up credentials from configuring user or something
485 | }
486 |
487 | @Override public Authentication getDefaultAuthentication(Queue.Item item) {
488 | return getDefaultAuthentication();
489 | }
490 |
491 | @Override public boolean isContinued() {
492 | return cookie != null; // in which case this is after a restart and we still claim the executor
493 | }
494 |
495 | private static void finish(@CheckForNull final String cookie) {
496 | if (cookie == null) {
497 | return;
498 | }
499 | synchronized (runningTasks) {
500 | final RunningTask runningTask = runningTasks.remove(cookie);
501 | if (runningTask == null) {
502 | LOGGER.log(FINE, "no running task corresponds to {0}", cookie);
503 | return;
504 | }
505 | final AsynchronousExecution execution = runningTask.execution;
506 | if (execution == null) {
507 | // JENKINS-30759: finished before asynch execution was even scheduled
508 | return;
509 | }
510 | assert runningTask.launcher != null;
511 | Timer.get().submit(new Runnable() { // JENKINS-31614
512 | @Override public void run() {
513 | execution.completed(null);
514 | try {
515 | runningTask.launcher.kill(Collections.singletonMap(COOKIE_VAR, cookie));
516 | } catch (ChannelClosedException x) {
517 | // fine, Jenkins was shutting down
518 | } catch (RequestAbortedException x) {
519 | // slave was exiting; too late to kill subprocesses
520 | } catch (Exception x) {
521 | LOGGER.log(Level.WARNING, "failed to shut down " + cookie, x);
522 | }
523 | }
524 | });
525 | }
526 | }
527 |
528 | /**
529 | * Called when the body closure is complete.
530 | */
531 | @SuppressFBWarnings(value="SE_BAD_FIELD", justification="lease is pickled")
532 | private static final class Callback extends BodyExecutionCallback.TailCall {
533 |
534 | private final String cookie;
535 | private WorkspaceList.Lease lease;
536 |
537 | Callback(String cookie, WorkspaceList.Lease lease) {
538 | this.cookie = cookie;
539 | this.lease = lease;
540 | }
541 |
542 | @Override protected void finished(StepContext context) throws Exception {
543 | LOGGER.log(FINE, "finished {0}", cookie);
544 | lease.release();
545 | lease = null;
546 | finish(cookie);
547 | }
548 |
549 | }
550 |
551 | /**
552 | * Occupies {@link Executor} while workflow uses this slave.
553 | */
554 | @ExportedBean
555 | private final class PlaceholderExecutable implements ContinuableExecutable {
556 |
557 | @Override public void run() {
558 | final TaskListener listener;
559 | Launcher launcher;
560 | final Run, ?> r;
561 | try {
562 | Executor exec = Executor.currentExecutor();
563 | if (exec == null) {
564 | throw new IllegalStateException("running task without associated executor thread");
565 | }
566 | Computer computer = exec.getOwner();
567 | // Set up context for other steps inside this one.
568 | Node node = computer.getNode();
569 | if (node == null) {
570 | throw new IllegalStateException("running computer lacks a node");
571 | }
572 | listener = context.get(TaskListener.class);
573 | launcher = node.createLauncher(listener);
574 | r = context.get(Run.class);
575 | if (cookie == null) {
576 | // First time around.
577 | cookie = UUID.randomUUID().toString();
578 | // Switches the label to a self-label, so if the executable is killed and restarted via ExecutorPickle, it will run on the same node:
579 | label = computer.getName();
580 |
581 | EnvVars env = computer.getEnvironment();
582 | env.overrideAll(computer.buildEnvironment(listener));
583 | env.put(COOKIE_VAR, cookie);
584 | if (exec.getOwner() instanceof Jenkins.MasterComputer) {
585 | env.put("NODE_NAME", "master");
586 | } else {
587 | env.put("NODE_NAME", label);
588 | }
589 | env.put("EXECUTOR_NUMBER", String.valueOf(exec.getNumber()));
590 |
591 | synchronized (runningTasks) {
592 | runningTasks.put(cookie, new RunningTask());
593 | }
594 | // For convenience, automatically allocate a workspace, like WorkspaceStep would:
595 | Job,?> j = r.getParent();
596 | if (!(j instanceof TopLevelItem)) {
597 | throw new Exception(j + " must be a top-level job");
598 | }
599 | FilePath p = node.getWorkspaceFor((TopLevelItem) j);
600 | if (p == null) {
601 | throw new IllegalStateException(node + " is offline");
602 | }
603 | WorkspaceList.Lease lease = computer.getWorkspaceList().allocate(p);
604 | FilePath workspace = lease.path;
605 | FlowNode flowNode = context.get(FlowNode.class);
606 | flowNode.addAction(new WorkspaceActionImpl(workspace, flowNode));
607 | listener.getLogger().println("Running on " + computer.getDisplayName() + " in " + workspace); // TODO hyperlink
608 | context.newBodyInvoker()
609 | .withContexts(exec, computer, env, workspace)
610 | .withCallback(new Callback(cookie, lease))
611 | .start();
612 | LOGGER.log(FINE, "started {0}", cookie);
613 | } else {
614 | // just rescheduled after a restart; wait for task to complete
615 | LOGGER.log(FINE, "resuming {0}", cookie);
616 | }
617 | } catch (Exception x) {
618 | context.onFailure(x);
619 | return;
620 | }
621 | // wait until the invokeBodyLater call above completes and notifies our Callback object
622 | synchronized (runningTasks) {
623 | LOGGER.log(FINE, "waiting on {0}", cookie);
624 | RunningTask runningTask = runningTasks.get(cookie);
625 | if (runningTask == null) {
626 | LOGGER.log(FINE, "running task apparently finished quickly for {0}", cookie);
627 | return;
628 | }
629 | assert runningTask.execution == null;
630 | assert runningTask.launcher == null;
631 | runningTask.launcher = launcher;
632 | runningTask.execution = new AsynchronousExecution() {
633 | @Override public void interrupt(boolean forShutdown) {
634 | if (forShutdown) {
635 | return;
636 | }
637 | LOGGER.log(FINE, "interrupted {0}", cookie);
638 | // TODO save the BodyExecution somehow and call .cancel() here; currently we just interrupt the build as a whole:
639 | Executor masterExecutor = r.getExecutor();
640 | if (masterExecutor != null) {
641 | masterExecutor.interrupt();
642 | } else { // ?
643 | super.getExecutor().recordCauseOfInterruption(r, listener);
644 | }
645 | }
646 | @Override public boolean blocksRestart() {
647 | return false;
648 | }
649 | @Override public boolean displayCell() {
650 | return true;
651 | }
652 | };
653 | throw runningTask.execution;
654 | }
655 | }
656 |
657 | @Override public PlaceholderTask getParent() {
658 | return PlaceholderTask.this;
659 | }
660 |
661 | @Override public long getEstimatedDuration() {
662 | return getParent().getEstimatedDuration();
663 | }
664 |
665 | @Override public boolean willContinue() {
666 | synchronized (runningTasks) {
667 | return runningTasks.containsKey(cookie);
668 | }
669 | }
670 |
671 | @Restricted(DoNotUse.class) // for Jelly
672 | public @CheckForNull Executor getExecutor() {
673 | return Executor.of(this);
674 | }
675 |
676 | @Restricted(NoExternalUse.class) // for Jelly and toString
677 | public String getUrl() {
678 | return PlaceholderTask.this.getUrl(); // we hope this has a console.jelly
679 | }
680 |
681 | @Override public String toString() {
682 | return "PlaceholderExecutable:" + getUrl() + ":" + cookie;
683 | }
684 |
685 | private static final long serialVersionUID = 1L;
686 | }
687 | }
688 |
689 | private static final long serialVersionUID = 1L;
690 |
691 | private static final Logger LOGGER = Logger.getLogger(DockerNodeStepExecution.class.getName());
692 |
693 | }
694 |
--------------------------------------------------------------------------------