├── trampoline ├── .gitignore ├── Makefile └── src │ └── trampoline │ └── main.go ├── docs ├── config.png └── docker.png ├── Jenkinsfile ├── src └── main │ ├── webapp │ └── images │ │ ├── 16x16 │ │ └── docker-logo.png │ │ ├── 24x24 │ │ └── docker-logo.png │ │ └── 48x48 │ │ └── docker-logo.png │ ├── resources │ ├── it │ │ └── dockins │ │ │ └── dockerslaves │ │ │ ├── trampoline │ │ │ ├── hints │ │ │ └── MemoryHint │ │ │ │ ├── help-memory.html │ │ │ │ └── config.jelly │ │ │ ├── pipeline │ │ │ ├── Messages.properties │ │ │ └── DockerNodeStep │ │ │ │ └── config.jelly │ │ │ ├── spec │ │ │ ├── DockerSocketContainerDefinition │ │ │ │ └── config.jelly │ │ │ ├── ContainerSetDefinition │ │ │ │ ├── help-sideContainers.html │ │ │ │ ├── help-buildHostImage.html │ │ │ │ └── config.jelly │ │ │ ├── ImageIdContainerDefinition │ │ │ │ ├── help-forcePull.html │ │ │ │ └── configure-entries.jelly │ │ │ ├── SideContainerDefinition │ │ │ │ └── config.jelly │ │ │ ├── ContainerDefinition │ │ │ │ └── config.jelly │ │ │ └── DockerfileContainerDefinition │ │ │ │ └── configure-entries.jelly │ │ │ ├── DefaultDockerHostSource │ │ │ └── config.jelly │ │ │ ├── ContainersContext │ │ │ ├── badge.jelly │ │ │ └── index.jelly │ │ │ ├── DefaultDockerProvisionerFactory │ │ │ ├── help-remotingImage.html │ │ │ ├── help-scmImage.html │ │ │ └── config.jelly │ │ │ ├── drivers │ │ │ └── PlainDockerAPIDockerDriverFactory │ │ │ │ └── config.jelly │ │ │ └── DockerSlaves │ │ │ ├── help-defaultBuildContainerImageName.html │ │ │ └── config.jelly │ └── index.jelly │ └── java │ └── it │ └── dockins │ └── dockerslaves │ ├── api │ ├── README.md │ ├── OneShotExecutorProvisioningException.java │ ├── OneShotComputer.java │ └── OneShotSlave.java │ ├── WorkspaceVolumeStrategy.java │ ├── spec │ ├── HintDescriptor.java │ ├── Hint.java │ ├── ContainerDefinitionDescriptor.java │ ├── ContainerDefinition.java │ ├── SideContainerDefinition.java │ ├── DockerSocketContainerDefinition.java │ ├── ImageIdContainerDefinition.java │ ├── DockerfileContainerDefinition.java │ └── ContainerSetDefinition.java │ ├── spi │ ├── DockerHostSourceDescriptor.java │ ├── DockerDriverFactoryDescriptor.java │ ├── DockerProvisionerFactoryDescriptor.java │ ├── DockerDriverFactory.java │ ├── DockerHostSource.java │ ├── DockerProvisionerFactory.java │ ├── DockerProvisioner.java │ ├── DockerHostConfig.java │ └── DockerDriver.java │ ├── DockerWorkspace.java │ ├── Container.java │ ├── hints │ ├── VolumeHint.java │ └── MemoryHint.java │ ├── ContainerSpecEnvironmentContributor.java │ ├── DefaultDockerHostSource.java │ ├── ProvisionScheduler.java │ ├── drivers │ ├── PlainDockerAPIDockerDriverFactory.java │ └── CliDockerDriver.java │ ├── DockerSlaveAssignmentAction.java │ ├── DockerComputerLauncher.java │ ├── DockerLauncher.java │ ├── DockerComputer.java │ ├── ContainersContext.java │ ├── pipeline │ ├── DockerNodeStep.java │ └── DockerNodeStepExecution.java │ ├── DefaultDockerProvisionerFactory.java │ ├── ProvisionQueueListener.java │ ├── DockerSlave.java │ ├── DockerSlaves.java │ └── DefaultDockerProvisioner.java ├── .gitignore ├── TODO.md ├── Workflow.md ├── Docker.md ├── Architecture.md ├── pom.xml └── README.md /trampoline/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /docs/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/docker-slaves-plugin/HEAD/docs/config.png -------------------------------------------------------------------------------- /docs/docker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/docker-slaves-plugin/HEAD/docs/docker.png -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | 3 | /* `buildPlugin` step provided by: https://github.com/jenkins-infra/pipeline-library */ 4 | buildPlugin() 5 | -------------------------------------------------------------------------------- /src/main/webapp/images/16x16/docker-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/docker-slaves-plugin/HEAD/src/main/webapp/images/16x16/docker-logo.png -------------------------------------------------------------------------------- /src/main/webapp/images/24x24/docker-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/docker-slaves-plugin/HEAD/src/main/webapp/images/24x24/docker-logo.png -------------------------------------------------------------------------------- /src/main/webapp/images/48x48/docker-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/docker-slaves-plugin/HEAD/src/main/webapp/images/48x48/docker-logo.png -------------------------------------------------------------------------------- /src/main/resources/it/dockins/dockerslaves/trampoline: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/docker-slaves-plugin/HEAD/src/main/resources/it/dockins/dockerslaves/trampoline -------------------------------------------------------------------------------- /src/main/resources/it/dockins/dockerslaves/hints/MemoryHint/help-memory.html: -------------------------------------------------------------------------------- 1 | Define Memory requirements for container 2 | format: <number><unit>. Number is a positive integer. Unit can be one of b, k, m, or g. Minimum is 4m. -------------------------------------------------------------------------------- /src/main/java/it/dockins/dockerslaves/api/README.md: -------------------------------------------------------------------------------- 1 | This "api" mimic one-shot-executor (https://wiki.jenkins-ci.org/display/JENKINS/One-Shot+Executor) 2 | plan is to switch to this plugin once the required jenkins-core changes are part of a LTS release. -------------------------------------------------------------------------------- /src/main/java/it/dockins/dockerslaves/WorkspaceVolumeStrategy.java: -------------------------------------------------------------------------------- 1 | package it.dockins.dockerslaves; 2 | 3 | /** 4 | * @author Nicolas De Loof 5 | */ 6 | public interface WorkspaceVolumeStrategy { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/it/dockins/dockerslaves/pipeline/Messages.properties: -------------------------------------------------------------------------------- 1 | DockerNodeStepExecution.PlaceholderTask.displayName=part of {0} 2 | DockerNodeStepExecution.PlaceholderTask.displayName_unknown=Unknown Pipeline node step 3 | DockerNodeStepExecution.queue_task_cancelled=Queue task was cancelled 4 | -------------------------------------------------------------------------------- /src/main/java/it/dockins/dockerslaves/spec/HintDescriptor.java: -------------------------------------------------------------------------------- 1 | package it.dockins.dockerslaves.spec; 2 | 3 | import hudson.model.Descriptor; 4 | 5 | /** 6 | * @author Nicolas De Loof 7 | */ 8 | public abstract class HintDescriptor extends Descriptor { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/it/dockins/dockerslaves/spi/DockerHostSourceDescriptor.java: -------------------------------------------------------------------------------- 1 | package it.dockins.dockerslaves.spi; 2 | 3 | import hudson.model.Descriptor; 4 | 5 | /** 6 | * @author Nicolas De Loof 7 | */ 8 | public abstract class DockerHostSourceDescriptor extends Descriptor { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/it/dockins/dockerslaves/spi/DockerDriverFactoryDescriptor.java: -------------------------------------------------------------------------------- 1 | package it.dockins.dockerslaves.spi; 2 | 3 | import hudson.model.Descriptor; 4 | 5 | /** 6 | * @author Nicolas De Loof 7 | */ 8 | public abstract class DockerDriverFactoryDescriptor extends Descriptor { 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pom.xml.tag 3 | pom.xml.releaseBackup 4 | pom.xml.versionsBackup 5 | pom.xml.next 6 | release.properties 7 | dependency-reduced-pom.xml 8 | buildNumber.properties 9 | work*/ 10 | 11 | # IntelliJ project files 12 | *.iml 13 | *.ipr 14 | *.iws 15 | .idea/ 16 | 17 | # eclipse project file 18 | .settings 19 | .classpath 20 | .project 21 | build/ 22 | -------------------------------------------------------------------------------- /src/main/java/it/dockins/dockerslaves/spi/DockerProvisionerFactoryDescriptor.java: -------------------------------------------------------------------------------- 1 | package it.dockins.dockerslaves.spi; 2 | 3 | import hudson.model.Descriptor; 4 | 5 | /** 6 | * @author Nicolas De Loof 7 | */ 8 | public abstract class DockerProvisionerFactoryDescriptor extends Descriptor { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/it/dockins/dockerslaves/spec/Hint.java: -------------------------------------------------------------------------------- 1 | package it.dockins.dockerslaves.spec; 2 | 3 | import hudson.ExtensionPoint; 4 | import hudson.model.AbstractDescribableImpl; 5 | 6 | /** 7 | * @author Nicolas De Loof 8 | */ 9 | public abstract class Hint extends AbstractDescribableImpl implements ExtensionPoint { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | ## Stuff to work on before releasing 0.1 4 | 5 | - [ ] Set environment variables in builds container [JENKINS-30538](https://issues.jenkins-ci.org/browse/JENKINS-30538) 6 | - [ ] Update `/etc/group` and `/etc/passwd` to add jenkins:jenkins [JENKINS-30539](https://issues.jenkins-ci.org/browse/JENKINS-30539) 7 | - [ ] Use job listener instead of slave listener where possible [JENKINS-30540](https://issues.jenkins-ci.org/browse/JENKINS-30540) 8 | -------------------------------------------------------------------------------- /src/main/java/it/dockins/dockerslaves/spi/DockerDriverFactory.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 | public abstract class DockerDriverFactory extends AbstractDescribableImpl implements ExtensionPoint { 10 | 11 | public abstract DockerDriver forJob(Job context) throws IOException, InterruptedException; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /Workflow.md: -------------------------------------------------------------------------------- 1 | # Workflow 2 | 3 | This document is a design doc. 4 | 5 | We'd like the docker-slaves plugin to be usable with [workflow plugin](https://github.com/jenkinsci/workflow-plugin) 6 | 7 | ### Proposed syntax : 8 | ``` 9 | withContainers( "maven:3-jdk8", database:"mysql", webserver:"jetty:9" ) { 10 | // some build steps 11 | } 12 | ``` 13 | 14 | ### Implementation notes 15 | plugin will implement `org.jenkinsci.plugins.workflow.support.steps.ExecutorStep` tio offer an alternative to `node()` 16 | -------------------------------------------------------------------------------- /src/main/java/it/dockins/dockerslaves/DockerWorkspace.java: -------------------------------------------------------------------------------- 1 | package it.dockins.dockerslaves; 2 | 3 | import hudson.Extension; 4 | import hudson.FilePath; 5 | import hudson.model.Node; 6 | import hudson.model.TopLevelItem; 7 | import jenkins.slaves.WorkspaceLocator; 8 | 9 | /** 10 | * 11 | * @author Nicolas De Loof 12 | */ 13 | @Extension 14 | public class DockerWorkspace extends WorkspaceLocator { 15 | 16 | @Override 17 | public FilePath locate(TopLevelItem item, Node node) { 18 | return null; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/it/dockins/dockerslaves/Container.java: -------------------------------------------------------------------------------- 1 | package it.dockins.dockerslaves; 2 | 3 | public class Container { 4 | final String imageName; 5 | String id; 6 | 7 | public Container(String imageName) { 8 | this.imageName = imageName; 9 | } 10 | 11 | public Container(String image, String id) { 12 | this(image); 13 | this.id = id; 14 | } 15 | 16 | public String getImageName() { 17 | return imageName; 18 | } 19 | 20 | public String getId() { 21 | return id; 22 | } 23 | 24 | public void setId(String id) { 25 | this.id = id; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/it/dockins/dockerslaves/spec/ContainerDefinitionDescriptor.java: -------------------------------------------------------------------------------- 1 | package it.dockins.dockerslaves.spec; 2 | 3 | import hudson.model.Descriptor; 4 | import jenkins.model.Jenkins; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * @author Nicolas De Loof 10 | */ 11 | public class ContainerDefinitionDescriptor extends Descriptor { 12 | 13 | public static List 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 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 | ![Configuration](docs/config.png) 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 | ![Docker implementation](docs/docker.png) 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 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 | --------------------------------------------------------------------------------