diff --git a/src/main/java/org/jenkinsci/plugins/workflow/steps/RetainEnvStep.java b/src/main/java/org/jenkinsci/plugins/workflow/steps/RetainEnvStep.java new file mode 100644 index 00000000..85f81b17 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/workflow/steps/RetainEnvStep.java @@ -0,0 +1,167 @@ +/* + * The MIT License + * + * Copyright (c) 2020, 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.workflow.steps; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import hudson.EnvVars; +import hudson.Extension; +import net.sf.json.JSONObject; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.StaplerRequest; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * All the variables not provided in the parameter will be filtered out for the enclosed block + */ +public class RetainEnvStep extends Step { + + /** + * Environment variable to keep. + */ + private final List variables; + + @DataBoundConstructor + public RetainEnvStep(List variables) { + this.variables = new ArrayList<>(variables); + } + + public List getVariables() { + return variables; + } + + @Override + public StepExecution start(StepContext context) throws Exception { + return new Execution(variables, context); + } + + public static class Execution extends AbstractStepExecutionImpl { + + private static final long serialVersionUID = 1; + + @SuppressFBWarnings(value="SE_TRANSIENT_FIELD_NOT_RESTORED", justification="Only used when starting.") + private transient final List variables; + + Execution(List variables, StepContext context) { + super(context); + this.variables = variables; + } + + @Override + public boolean start() throws Exception { + getContext().newBodyInvoker() + .withContext(EnvironmentExpander.merge( + getContext().get(EnvironmentExpander.class), + new FilteredEnvironmentExpander(variables) + )) + .withCallback(BodyExecutionCallback.wrap(getContext())) + .start(); + return false; + } + + @Override + public void onResume() {} + } + + @Extension + public static class DescriptorImpl extends StepDescriptor { + + @Override + public String getFunctionName() { + return "retainEnv"; + } + + @Override + public String getDisplayName() { + return "Keep only specified environment variables"; + } + + @Override + public boolean takesImplicitBlockArgument() { + return true; + } + + // TODO JENKINS-27901: need a standard control for this + @Override + public Step newInstance(StaplerRequest req, JSONObject formData) throws FormException { + String variablesToKeepS = formData.getString("variables"); + List variablesToKeep = new ArrayList<>(); + for (String line : variablesToKeepS.split("\r?\n")) { + line = line.trim(); + if (!line.isEmpty()) { + variablesToKeep.add(line); + } + } + return new RetainEnvStep(variablesToKeep); + } + + @Override + public Set> getRequiredContext() { + return Collections.emptySet(); + } + + @Override + public String argumentsToString(Map namedArgs) { + Object variablesToKeep = namedArgs.get("variables"); + if (variablesToKeep instanceof List) { + StringBuilder b = new StringBuilder(); + for (Object variableName : (List) variablesToKeep) { + if (variableName instanceof String) { + if (b.length() > 0) { + b.append(", "); + } + b.append((String) variableName); + } + } + return b.toString(); + } else { + return null; + } + } + } + + public static class FilteredEnvironmentExpander extends EnvironmentExpander { + private static final long serialVersionUID = 1; + + private final List variables; + + public FilteredEnvironmentExpander(Collection variables) { + this.variables = /* ensure serializability*/ new ArrayList<>(variables); + } + + @Override + public void expand(EnvVars env) { + List keyList = new ArrayList<>(env.keySet()); + keyList.removeAll(variables); + for (String key : keyList) { + env.remove(key); + } + } + } +} diff --git a/src/main/resources/org/jenkinsci/plugins/workflow/steps/RetainEnvStep/config.groovy b/src/main/resources/org/jenkinsci/plugins/workflow/steps/RetainEnvStep/config.groovy new file mode 100644 index 00000000..8631c2c1 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/workflow/steps/RetainEnvStep/config.groovy @@ -0,0 +1,29 @@ +/* + * The MIT License + * + * Copyright (c) 2020, 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.workflow.steps.RetainEnvStep + +f = namespace(lib.FormTagLib) +f.entry(field: 'variables', title: _('keepOnly')) { + f.textarea(value: instance == null ? '' : instance.variables.join('\n')) +} diff --git a/src/main/resources/org/jenkinsci/plugins/workflow/steps/RetainEnvStep/config.properties b/src/main/resources/org/jenkinsci/plugins/workflow/steps/RetainEnvStep/config.properties new file mode 100644 index 00000000..d8d1ffb3 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/workflow/steps/RetainEnvStep/config.properties @@ -0,0 +1 @@ +keepOnly=Keep only these environment variables diff --git a/src/main/resources/org/jenkinsci/plugins/workflow/steps/RetainEnvStep/help-variables.html b/src/main/resources/org/jenkinsci/plugins/workflow/steps/RetainEnvStep/help-variables.html new file mode 100644 index 00000000..184b3f54 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/workflow/steps/RetainEnvStep/help-variables.html @@ -0,0 +1,4 @@ +
+ A list of environment variables to retain. + All the other variables proposed by Jenkins will be filtered out for the block. +
diff --git a/src/main/resources/org/jenkinsci/plugins/workflow/steps/RetainEnvStep/help.html b/src/main/resources/org/jenkinsci/plugins/workflow/steps/RetainEnvStep/help.html new file mode 100644 index 00000000..3870d460 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/workflow/steps/RetainEnvStep/help.html @@ -0,0 +1,16 @@ +
+Filters out for the scope of the block, the environment variables proposed by Jenkins except the ones listed. +These are available to any external processes spawned within that scope. +For example: +

+node {
+  env.CUSTOM=test
+  env.INFO=useful
+  retainEnv(['INFO']) {
+    // at this point the CUSTOM will not be available
+    bat 'echo %INFO%'
+  }
+}
+
+

See the documentation for the env singleton for more information on environment variables. +

diff --git a/src/test/java/org/jenkinsci/plugins/workflow/steps/RetainEnvStepTest.java b/src/test/java/org/jenkinsci/plugins/workflow/steps/RetainEnvStepTest.java new file mode 100644 index 00000000..3cd0ba28 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/workflow/steps/RetainEnvStepTest.java @@ -0,0 +1,115 @@ +/* + * The MIT License + * + * Copyright (c) 2020, 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.workflow.steps; + +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import org.hamcrest.Matchers; +import org.jenkinsci.plugins.workflow.actions.ArgumentsAction; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.cps.nodes.StepStartNode; +import org.jenkinsci.plugins.workflow.graph.FlowNode; +import org.jenkinsci.plugins.workflow.graphanalysis.DepthFirstScanner; +import org.jenkinsci.plugins.workflow.graphanalysis.NodeStepTypePredicate; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.BuildWatcher; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +public class RetainEnvStepTest { + + @ClassRule + public static BuildWatcher buildWatcher = new BuildWatcher(); + + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Test + public void retainOnlyThePassedVariables() throws Exception { + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "env.A = 'value-a'\n" + + "env.B = 'value-b'\n" + + "env.C = 'value-c'\n" + + "node {\n" + + " isUnix() ? sh('echo a-b-c-1 A=$A B=$B C=$C D=$D end') : bat('echo a-b-c-1 A=%A% B=%B% C=%C% D=%D% end')\n" + + " retainEnv(['A', 'B']){\n" + + " isUnix() ? sh('echo only-a-b A=$A B=$B C=$C D=$D end') : bat('echo only-a-b A=%A% B=%B% C=%C% D=%D% end')\n" + + " withEnv(['D=value-d']){\n" + + " isUnix() ? sh('echo with-d A=$A B=$B C=$C D=$D end') : bat('echo with-d A=%A% B=%B% C=%C% D=%D% end')\n" + + " retainEnv(['B', 'D']){\n" + + " isUnix() ? sh('echo only-b-d A=$A B=$B C=$C D=$D end') : bat('echo only-b-d A=%A% B=%B% C=%C% D=%D% end')\n" + + " }\n" + + " }\n" + + " retainEnv(['A']){\n" + + " isUnix() ? sh('echo only-a A=$A B=$B C=$C D=$D end') : bat('echo only-a A=%A% B=%B% C=%C% D=%D% end')\n" + + " }\n" + + " retainEnv(['B']){\n" + + " isUnix() ? sh('echo only-b A=$A B=$B C=$C D=$D end') : bat('echo only-b A=%A% B=%B% C=%C% D=%D% end')\n" + + " }\n" + + " }\n" + + " isUnix() ? sh('echo a-b-c-2 A=$A B=$B C=$C D=$D end') : bat('echo a-b-c-2 A=%A% B=%B% C=%C% D=%D% end')\n" + + "}", true)); + WorkflowRun b = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + j.assertLogContains("a-b-c-1 A=value-a B=value-b C=value-c D= end", b); + j.assertLogContains("only-a-b A=value-a B=value-b C= D= end", b); + j.assertLogContains("with-d A=value-a B=value-b C= D=value-d end", b); + j.assertLogContains("only-b-d A= B=value-b C= D=value-d end", b); + j.assertLogContains("only-a A=value-a B= C= D= end", b); + j.assertLogContains("only-b A= B=value-b C= D= end", b); + j.assertLogContains("a-b-c-2 A=value-a B=value-b C=value-c D= end", b); + List coreStepNodes = new DepthFirstScanner().filteredNodes(b.getExecution(), Predicates.and(new NodeStepTypePredicate("retainEnv"), new Predicate() { + @Override public boolean apply(FlowNode n) { + return n instanceof StepStartNode && !((StepStartNode) n).isBody(); + } + })); + assertThat(coreStepNodes, Matchers.hasSize(4)); + assertEquals("A, B", ArgumentsAction.getStepArgumentsAsString(coreStepNodes.get(3))); + assertEquals("B, D", ArgumentsAction.getStepArgumentsAsString(coreStepNodes.get(2))); + assertEquals("A", ArgumentsAction.getStepArgumentsAsString(coreStepNodes.get(1))); + assertEquals("B", ArgumentsAction.getStepArgumentsAsString(coreStepNodes.get(0))); + } + + @Test + public void configRoundTrip() throws Exception { + configRoundTrip(Collections.emptyList()); + configRoundTrip(Collections.singletonList("VAR")); + configRoundTrip(Arrays.asList("VAR1", "VAR2")); + } + + private void configRoundTrip(List variablesToKeep) throws Exception { + assertEquals(variablesToKeep, new StepConfigTester(j).configRoundTrip(new RetainEnvStep(variablesToKeep)).getVariables()); + } +}