Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[FIXED JENKINS-49558] Add containerPerStage option
If given with a top-level Docker or Dockerfile agent, the top-level
will magically switch to just a label agent, while each stage without
an explicit agent specified will end up using the root agent
definition with reuseNode enabled. The result will be that each stage
gets a new container. Tada.

This could theoretically cause problems in resuming builds after
upgrade, but not likely, since we actually instantiate the
DeclarativeAgent instance just in time, not at the beginning of the
build. So the new field AbstractDockerAgent#containerPerStageRoot
should get populated correctly.
  • Loading branch information
abayer committed Feb 20, 2018
1 parent b492733 commit 773530a
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 6 deletions.
Expand Up @@ -26,11 +26,13 @@ package org.jenkinsci.plugins.pipeline.modeldefinition.model
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import org.jenkinsci.plugins.pipeline.modeldefinition.agent.AbstractDockerAgent
import org.jenkinsci.plugins.pipeline.modeldefinition.agent.DeclarativeAgent
import org.jenkinsci.plugins.pipeline.modeldefinition.agent.DeclarativeAgentDescriptor
import org.jenkinsci.plugins.pipeline.modeldefinition.agent.impl.None
import org.jenkinsci.plugins.pipeline.modeldefinition.options.DeclarativeOption
import org.jenkinsci.plugins.pipeline.modeldefinition.options.impl.CheckoutToSubdirectory
import org.jenkinsci.plugins.pipeline.modeldefinition.options.impl.ContainerPerStage
import org.jenkinsci.plugins.pipeline.modeldefinition.options.impl.SkipDefaultCheckout
import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted
import org.jenkinsci.plugins.structs.SymbolLookup
Expand Down Expand Up @@ -102,6 +104,18 @@ class Agent extends MappedClosure<Object,Agent> implements Serializable {
if (subdir?.subdirectory != null && subdir?.subdirectory != "") {
a.setSubdirectory(subdir.subdirectory)
}
if (a instanceof AbstractDockerAgent) {
ContainerPerStage containerPerStage = (ContainerPerStage) options.get("containerPerStage")
if (containerPerStage != null) {
if (context instanceof Root) {
// If we're on the root, make sure we switch to basically just doing a label
a.containerPerStageRoot = true
} else if (context instanceof Stage && context.agent == null) {
// While if we're on a stage that doesn't have an explicit agent, make sure we reuse the node
a.reuseNode = true
}
}
}
}
a.setDoCheckout(doCheckout)

Expand Down
Expand Up @@ -36,6 +36,7 @@ public abstract class AbstractDockerAgent<D extends AbstractDockerAgent<D>> exte
protected String registryCredentialsId;
protected String customWorkspace;
protected boolean reuseNode;
protected boolean containerPerStageRoot;

public @Nullable
String getRegistryUrl() {
Expand Down Expand Up @@ -92,4 +93,13 @@ public void setCustomWorkspace(String customWorkspace) {
public void setArgs(String args) {
this.args = args;
}

public boolean isContainerPerStageRoot() {
return containerPerStageRoot;
}

@DataBoundSetter
public void setContainerPerStageRoot(boolean containerPerStageRoot) {
this.containerPerStageRoot = containerPerStageRoot;
}
}
@@ -0,0 +1,42 @@
/*
* The MIT License
*
* Copyright (c) 2018, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package org.jenkinsci.plugins.pipeline.modeldefinition.options.impl;

import hudson.Extension;
import org.jenkinsci.Symbol;
import org.jenkinsci.plugins.pipeline.modeldefinition.options.DeclarativeOption;
import org.jenkinsci.plugins.pipeline.modeldefinition.options.DeclarativeOptionDescriptor;
import org.kohsuke.stapler.DataBoundConstructor;

public class ContainerPerStage extends DeclarativeOption {

@DataBoundConstructor
public ContainerPerStage() {}

@Extension @Symbol("containerPerStage")
public static class DescriptorImpl extends DeclarativeOptionDescriptor {

}
}
Expand Up @@ -28,6 +28,7 @@ import com.cloudbees.groovy.cps.impl.CpsClosure
import hudson.FilePath
import hudson.Launcher
import hudson.model.Result
import org.jenkinsci.plugins.pipeline.modeldefinition.agent.AbstractDockerAgent
import org.jenkinsci.plugins.pipeline.modeldefinition.model.*
import org.jenkinsci.plugins.pipeline.modeldefinition.options.DeclarativeOption
import org.jenkinsci.plugins.pipeline.modeldefinition.steps.CredentialWrapper
Expand Down Expand Up @@ -483,6 +484,11 @@ class ModelInterpreter implements Serializable {
* @return The return of the resulting executed closure
*/
def inDeclarativeAgent(Object context, Root root, Agent agent, Closure body) {
if (agent == null
&& root.agent.getDeclarativeAgent(root, root) instanceof AbstractDockerAgent
&& root.options?.options?.get("containerPerStage") != null) {
agent = root.agent
}
if (agent == null) {
return {
body.call()
Expand Down
Expand Up @@ -43,18 +43,25 @@ abstract class AbstractDockerPipelineScript<A extends AbstractDockerAgent<A>> ex
return {
configureRegistry(body).call()
}
} else if (describable.containerPerStageRoot) {
return getLabelScript().run {
body.call()
}
} else {
String targetLabel = DeclarativeDockerUtils.getLabel(describable.label)
Label l = (Label) Label.DescriptorImpl.instanceForName("label", [label: targetLabel])
l.copyFlags(describable)
l.customWorkspace = describable.customWorkspace
LabelScript labelScript = (LabelScript) l.getScript(script)
return labelScript.run {
return getLabelScript().run {
configureRegistry(body).call()
}
}
}

protected LabelScript getLabelScript() {
String targetLabel = DeclarativeDockerUtils.getLabel(describable.label)
Label l = (Label) Label.DescriptorImpl.instanceForName("label", [label: targetLabel])
l.copyFlags(describable)
l.customWorkspace = describable.customWorkspace
return (LabelScript) l.getScript(script)
}

protected Closure configureRegistry(Closure body) {
return {
String registryUrl = DeclarativeDockerUtils.getRegistryUrl(describable.registryUrl)
Expand Down
@@ -0,0 +1,27 @@
<!--
~ The MIT License
~
~ Copyright (c) 2018, CloudBees, Inc.
~
~ Permission is hereby granted, free of charge, to any person obtaining a copy
~ of this software and associated documentation files (the "Software"), to deal
~ in the Software without restriction, including without limitation the rights
~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
~ copies of the Software, and to permit persons to whom the Software is
~ furnished to do so, subject to the following conditions:
~
~ The above copyright notice and this permission notice shall be included in
~ all copies or substantial portions of the Software.
~
~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
~ THE SOFTWARE.
-->

<p>
If specified, any stage using the top-level Docker or Dockerfile agent will get a new container on the same node.
</p>
Expand Up @@ -89,6 +89,18 @@ public void agentDockerReuseNode() throws Exception {
agentDocker("agentDockerReuseNode");
}

@Issue("JENKINS-49558")
@Test
public void agentDockerContainerPerStage() throws Exception {
agentDocker("agentDockerContainerPerStage");
}

@Issue("JENKINS-49558")
@Test
public void agentDockerWithoutContainerPerStage() throws Exception {
agentDocker("agentDockerWithoutContainerPerStage");
}

@Test
public void agentDockerDontReuseNode() throws Exception {
assumeDocker();
Expand Down
@@ -0,0 +1,62 @@
/*
* The MIT License
*
* Copyright (c) 2018, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

pipeline {
agent {
docker {
image "httpd:2.4.12"
label "docker"
}
}
options {
containerPerStage()
}
stages {
stage("foo") {
steps {
sh 'ls -la'
sh 'echo "The answer is 42"'
sh 'echo "${NODE_NAME}" > tmp.txt'
sh 'echo $HOSTNAME > host.txt'
}
}
stage("bar") {
steps {
sh 'test -f Jenkinsfile'
sh 'test -f tmp.txt'
script {
def oldHn = readFile('host.txt')
def newHn = sh(script:'echo $HOSTNAME', returnStdout:true)
if (oldHn == newHn) {
error("HOSTNAMES SHOULD NOT MATCH")
}
}
}

}
}
}



@@ -0,0 +1,59 @@
/*
* The MIT License
*
* Copyright (c) 2018, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

pipeline {
agent {
docker {
image "httpd:2.4.12"
label "docker"
}
}
stages {
stage("foo") {
steps {
sh 'ls -la'
sh 'echo "The answer is 42"'
sh 'echo "${NODE_NAME}" > tmp.txt'
sh 'echo $HOSTNAME > host.txt'
}
}
stage("bar") {
steps {
sh 'test -f Jenkinsfile'
sh 'test -f tmp.txt'
script {
def oldHn = readFile('host.txt')
def newHn = sh(script:'echo $HOSTNAME', returnStdout:true)
if (oldHn != newHn) {
error("HOSTNAMES SHOULD MATCH")
}
}
}

}
}
}



0 comments on commit 773530a

Please sign in to comment.