diff --git a/core/src/main/java/hudson/Launcher.java b/core/src/main/java/hudson/Launcher.java index 850ead6bec94..53bc24f5ccba 100644 --- a/core/src/main/java/hudson/Launcher.java +++ b/core/src/main/java/hudson/Launcher.java @@ -26,6 +26,7 @@ import hudson.Proc.LocalProc; import hudson.model.Computer; import jenkins.util.MemoryReductionUtil; +import hudson.model.Run; import hudson.util.QuotedStringTokenizer; import jenkins.model.Jenkins; import hudson.model.TaskListener; @@ -39,8 +40,12 @@ import hudson.util.ArgumentListBuilder; import hudson.util.ProcessTree; import jenkins.security.MasterToSlaveCallable; +import jenkins.tasks.filters.EnvVarsFilterRuleWrapper; +import jenkins.tasks.filters.EnvVarsFilterLocalRule; +import jenkins.tasks.filters.EnvVarsFilterableBuilder; import org.apache.commons.io.input.NullInputStream; import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; import org.kohsuke.accmod.restrictions.NoExternalUse; import edu.umd.cs.findbugs.annotations.CheckForNull; @@ -90,6 +95,9 @@ public abstract class Launcher { @CheckForNull protected final VirtualChannel channel; + @Restricted(Beta.class) + protected EnvVarsFilterRuleWrapper envVarsFilterRuleWrapper; + public Launcher(@NonNull TaskListener listener, @CheckForNull VirtualChannel channel) { this.listener = listener; this.channel = channel; @@ -103,6 +111,25 @@ protected Launcher(@NonNull Launcher launcher) { this(launcher.listener, launcher.channel); } + /** + * Build the environment filter rules that will be applied on the environment variables + * @param run The run that requested the command interpretation, could be null if outside of a run context. + * @param builder The builder that asked to run this command + * + * @since TODO + */ + @Restricted(Beta.class) + public void prepareFilterRules(@CheckForNull Run run, @NonNull EnvVarsFilterableBuilder builder){ + List specificRuleList = builder.buildEnvVarsFilterRules(); + EnvVarsFilterRuleWrapper ruleWrapper = EnvVarsFilterRuleWrapper.createRuleWrapper(run, builder, this, specificRuleList); + this.setEnvVarsFilterRuleWrapper(ruleWrapper); + } + + @Restricted(Beta.class) + protected void setEnvVarsFilterRuleWrapper(EnvVarsFilterRuleWrapper envVarsFilterRuleWrapper) { + this.envVarsFilterRuleWrapper = envVarsFilterRuleWrapper; + } + /** * Gets the channel that can be used to run a program remotely. * @@ -171,6 +198,13 @@ public final class ProcStarter { protected InputStream stdin = NULL_INPUT_STREAM; @CheckForNull protected String[] envs = null; + /** + * Represent the build step, either from legacy build process or from pipeline one + */ + @CheckForNull + @Restricted(Beta.class) + protected EnvVarsFilterableBuilder envVarsFilterableBuilder = null; + /** * True to reverse the I/O direction. * @@ -446,6 +480,26 @@ public ProcStarter writeStdin() { return this; } + /** + * Specify the build step that want to run the command to enable the environment filters + * @return {@code this} + * @since TODO + */ + @Restricted(Beta.class) + public ProcStarter buildStep(EnvVarsFilterableBuilder envVarsFilterableBuilder){ + this.envVarsFilterableBuilder = envVarsFilterableBuilder; + return this; + } + + /** + * @return if set, returns the build step that wants to run the command + * @since TODO + */ + @Restricted(Beta.class) + public @CheckForNull + EnvVarsFilterableBuilder buildStep() { + return envVarsFilterableBuilder; + } /** * Starts the new process as configured. @@ -494,7 +548,7 @@ public int join() throws IOException, InterruptedException { */ @NonNull public ProcStarter copy() { - ProcStarter rhs = new ProcStarter().cmds(commands).pwd(pwd).masks(masks).stdin(stdin).stdout(stdout).stderr(stderr).envs(envs).quiet(quiet); + ProcStarter rhs = new ProcStarter().cmds(commands).pwd(pwd).masks(masks).stdin(stdin).stdout(stdout).stderr(stderr).envs(envs).quiet(quiet).buildStep(envVarsFilterableBuilder); rhs.stdoutListener = stdoutListener; rhs.reverseStdin = this.reverseStdin; rhs.reverseStderr = this.reverseStderr; @@ -924,6 +978,12 @@ public Proc launch(ProcStarter ps) throws IOException { EnvVars jobEnv = inherit(ps.envs); + if (envVarsFilterRuleWrapper != null) { + envVarsFilterRuleWrapper.filter(jobEnv, this, listener); + // reset the rules to prevent build step without rules configuration to re-use those + envVarsFilterRuleWrapper = null; + } + // replace variables in command line String[] jobCmd = new String[ps.commands.size()]; for ( int idx = 0 ; idx < jobCmd.length; idx++ ) @@ -1054,7 +1114,11 @@ public Proc launch(ProcStarter ps) throws IOException { final String workDir = psPwd==null ? null : psPwd.getRemote(); try { - return new ProcImpl(getChannel().call(new RemoteLaunchCallable(ps.commands, ps.masks, ps.envs, in, ps.reverseStdin, out, ps.reverseStdout, err, ps.reverseStderr, ps.quiet, workDir, listener, ps.stdoutListener))); + RemoteLaunchCallable remote = new RemoteLaunchCallable(ps.commands, ps.masks, ps.envs, in, ps.reverseStdin, out, ps.reverseStdout, err, ps.reverseStderr, ps.quiet, workDir, listener, ps.stdoutListener); + remote.setEnvVarsFilterRuleWrapper(envVarsFilterRuleWrapper); + // reset the rules to prevent build step without rules configuration to re-use those + envVarsFilterRuleWrapper = null; + return new ProcImpl(getChannel().call(remote)); } catch (InterruptedException e) { throw (IOException)new InterruptedIOException().initCause(e); } @@ -1279,8 +1343,10 @@ private static class RemoteLaunchCallable extends MasterToSlaveCallable cmd, @CheckForNull boolean[] masks, @CheckForNull String[] env, - @CheckForNull InputStream in, boolean reverseStdin, + private EnvVarsFilterRuleWrapper envVarsFilterRuleWrapper; + + RemoteLaunchCallable(@NonNull List cmd, @CheckForNull boolean[] masks, @CheckForNull String[] env, + @CheckForNull InputStream in, boolean reverseStdin, @CheckForNull OutputStream out, boolean reverseStdout, @CheckForNull OutputStream err, boolean reverseStderr, boolean quiet, @CheckForNull String workDir, @NonNull TaskListener listener, @CheckForNull TaskListener stdoutListener) { @@ -1299,9 +1365,17 @@ private static class RemoteLaunchCallable extends MasterToSlaveCallable configuredLocalRules) { + this.configuredLocalRules = configuredLocalRules; + } + private Integer unstableReturn; public String[] buildCommandLine(FilePath script) { @@ -80,9 +96,11 @@ protected boolean isErrorlevelForUnstableBuild(int exitCode) { return this.unstableReturn != null && exitCode != 0 && this.unstableReturn.equals(exitCode); } - private Object readResolve() throws ObjectStreamException { + private Object readResolve() { BatchFile batch = new BatchFile(command); batch.setUnstableReturn(unstableReturn); + // backward compatibility + batch.setConfiguredLocalRules(configuredLocalRules == null ? new ArrayList<>() : configuredLocalRules); return batch; } @@ -124,5 +142,11 @@ public FormValidation doCheckUnstableReturn(@QueryParameter String value) { public boolean isApplicable(Class jobType) { return true; } + + // used by Jelly view + @Restricted(NoExternalUse.class) + public List getApplicableLocalRules() { + return EnvVarsFilterLocalRuleDescriptor.allApplicableFor(BatchFile.class); + } } } diff --git a/core/src/main/java/hudson/tasks/CommandInterpreter.java b/core/src/main/java/hudson/tasks/CommandInterpreter.java index 64160f291504..6e8c2863f8c9 100644 --- a/core/src/main/java/hudson/tasks/CommandInterpreter.java +++ b/core/src/main/java/hudson/tasks/CommandInterpreter.java @@ -35,8 +35,16 @@ import hudson.model.Result; import hudson.model.TaskListener; import hudson.remoting.ChannelClosedException; +import jenkins.tasks.filters.EnvVarsFilterLocalRule; +import jenkins.tasks.filters.EnvVarsFilterableBuilder; +import jenkins.tasks.filters.EnvVarsFilterException; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; +import org.kohsuke.accmod.restrictions.NoExternalUse; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; @@ -47,12 +55,18 @@ * * @author Kohsuke Kawaguchi */ -public abstract class CommandInterpreter extends Builder { +public abstract class CommandInterpreter extends Builder implements EnvVarsFilterableBuilder { /** * Command to execute. The format depends on the actual {@link CommandInterpreter} implementation. */ protected final String command; + /** + * List of configured environment filter rules + */ + @Restricted(Beta.class) + protected List configuredLocalRules = new ArrayList<>(); + public CommandInterpreter(String command) { this.command = command; } @@ -61,6 +75,16 @@ public final String getCommand() { return command; } + public @NonNull List buildEnvVarsFilterRules() { + return new ArrayList<>(configuredLocalRules); + } + + // used by Jelly view + @Restricted(NoExternalUse.class) + public List getConfiguredLocalRules() { + return configuredLocalRules; + } + @Override public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException { return perform(build,launcher,(TaskListener)listener); @@ -106,7 +130,21 @@ public boolean perform(AbstractBuild build, Launcher launcher, TaskListener for(Map.Entry e : build.getBuildVariables().entrySet()) envVars.put(e.getKey(),e.getValue()); - r = join(launcher.launch().cmds(buildCommandLine(script)).envs(envVars).stdout(listener).pwd(ws).start()); + launcher.prepareFilterRules(build, this); + + Launcher.ProcStarter procStarter = launcher.launch(); + procStarter.cmds(buildCommandLine(script)) + .envs(envVars) + .stdout(listener) + .pwd(ws); + + try { + Proc proc = procStarter.start(); + r = join(proc); + } catch (EnvVarsFilterException se) { + LOGGER.log(Level.FINE, "Environment variable filtering failed", se); + return false; + } if(isErrorlevelForUnstableBuild(r)) { build.setResult(Result.UNSTABLE); diff --git a/core/src/main/java/hudson/tasks/Shell.java b/core/src/main/java/hudson/tasks/Shell.java index 1aa1502f0404..702b9897d3c1 100644 --- a/core/src/main/java/hudson/tasks/Shell.java +++ b/core/src/main/java/hudson/tasks/Shell.java @@ -31,14 +31,18 @@ import hudson.remoting.VirtualChannel; import hudson.util.FormValidation; import java.io.IOException; -import java.io.ObjectStreamException; + import hudson.util.LineEndingConversion; import jenkins.security.MasterToSlaveCallable; +import jenkins.tasks.filters.EnvVarsFilterLocalRule; +import jenkins.tasks.filters.EnvVarsFilterLocalRuleDescriptor; import net.sf.json.JSONObject; import org.apache.commons.lang.SystemUtils; import org.jenkinsci.Symbol; import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; import org.kohsuke.accmod.restrictions.DoNotUse; +import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.StaplerRequest; @@ -64,9 +68,18 @@ public Shell(String command) { super(LineEndingConversion.convertEOL(command, LineEndingConversion.EOLType.Unix)); } - private Integer unstableReturn; - + /** + * Set local environment variable filter rules + * @param configuredLocalRules list of local environment filter rules + * @since TODO + */ + @Restricted(Beta.class) + @DataBoundSetter + public void setConfiguredLocalRules(List configuredLocalRules) { + this.configuredLocalRules = configuredLocalRules; + } + private Integer unstableReturn; /** * Older versions of bash have a bug where non-ASCII on the first line @@ -124,9 +137,11 @@ public DescriptorImpl getDescriptor() { return (DescriptorImpl)super.getDescriptor(); } - private Object readResolve() throws ObjectStreamException { + private Object readResolve() { Shell shell = new Shell(command); shell.setUnstableReturn(unstableReturn); + // backward compatibility + shell.setConfiguredLocalRules(configuredLocalRules == null ? new ArrayList<>() : configuredLocalRules); return shell; } @@ -141,6 +156,12 @@ public boolean isApplicable(Class jobType) { return true; } + // used by Jelly view + @Restricted(NoExternalUse.class) + public List getApplicableLocalRules() { + return EnvVarsFilterLocalRuleDescriptor.allApplicableFor(Shell.class); + } + public String getShell() { return shell; } diff --git a/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterException.java b/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterException.java new file mode 100644 index 000000000000..49c1fc136cb7 --- /dev/null +++ b/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterException.java @@ -0,0 +1,77 @@ +/* + * 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 jenkins.tasks.filters; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.AbortException; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; + +/** + * Exception that occurs during the environment filtering process, with helper to track the source. + * + * @since TODO + */ +@Restricted(Beta.class) +public class EnvVarsFilterException extends AbortException { + private EnvVarsFilterRule rule; + private String variableName; + + public EnvVarsFilterException(String message) { + super(message); + } + + public @NonNull + EnvVarsFilterException withRule(@NonNull EnvVarsFilterRule rule) { + this.rule = rule; + return this; + } + + public @NonNull EnvVarsFilterException withVariable(@NonNull String variableName) { + this.variableName = variableName; + return this; + } + + public @CheckForNull + EnvVarsFilterRule getRule() { + return rule; + } + + @Override + public @NonNull String getMessage() { + String message = super.getMessage(); + if (variableName != null) { + message += " due to variable '" + variableName + "'"; + } + if (rule != null) { + if (rule instanceof EnvVarsFilterGlobalRule) { + message += " detected by the global rule " + rule.getDisplayName(); + } else { + message += " detected by " + rule.getDisplayName(); + } + } + return message; + } +} diff --git a/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterGlobalConfiguration.java b/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterGlobalConfiguration.java new file mode 100644 index 000000000000..5f8008200533 --- /dev/null +++ b/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterGlobalConfiguration.java @@ -0,0 +1,89 @@ +/* + * 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 jenkins.tasks.filters; + +import hudson.Extension; +import hudson.ExtensionList; +import hudson.model.Descriptor; +import hudson.util.DescribableList; +import jenkins.model.GlobalConfiguration; +import jenkins.model.GlobalConfigurationCategory; +import jenkins.model.Jenkins; +import net.sf.json.JSONObject; +import org.jenkinsci.Symbol; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; +import org.kohsuke.stapler.StaplerRequest; + +import java.io.IOException; +import java.util.List; + +/** + * Configuration of the filter rules that are applied globally, + * after filtering which rule applies on which builder + * + * @since TODO + */ +@Extension +@Symbol("envVarsFilter") +@Restricted(Beta.class) +public class EnvVarsFilterGlobalConfiguration extends GlobalConfiguration { + private DescribableList> activatedGlobalRules = + new DescribableList<>(this); + + public EnvVarsFilterGlobalConfiguration() { + load(); + } + + public static EnvVarsFilterGlobalConfiguration get() { + return GlobalConfiguration.all().get(EnvVarsFilterGlobalConfiguration.class); + } + + // used by Jelly + public static ExtensionList> getAllGlobalRules() { + return Jenkins.get().getDescriptorList(EnvVarsFilterGlobalRule.class); + } + + public static List getAllActivatedGlobalRules() { + return get().activatedGlobalRules; + } + + @Override + public GlobalConfigurationCategory getCategory() { + return GlobalConfigurationCategory.get(GlobalConfigurationCategory.Unclassified.class); + } + + @Override + public boolean configure(StaplerRequest req, JSONObject json) throws FormException { + try { + activatedGlobalRules.rebuildHetero(req, json, getAllGlobalRules(), "rules"); + } catch (IOException e) { + throw new FormException(e, "rules"); + } + + this.save(); + + return true; + } +} diff --git a/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterGlobalRule.java b/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterGlobalRule.java new file mode 100644 index 000000000000..4f1bd76110c1 --- /dev/null +++ b/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterGlobalRule.java @@ -0,0 +1,61 @@ +/* + * 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 jenkins.tasks.filters; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.ExtensionPoint; +import hudson.Launcher; +import hudson.model.Describable; +import hudson.model.Descriptor; +import hudson.model.Run; +import jenkins.model.Jenkins; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; + +import java.io.Serializable; + +/** + * Environment variables filter rule that is configured globally for all jobs.

+ * The job types can be filtered using {@link #isApplicable(Run, Object, Launcher)} + * + * The local rules are applied before the global ones. + * + * @since TODO + */ +@Restricted(Beta.class) +public interface EnvVarsFilterGlobalRule extends Describable, EnvVarsFilterRule, ExtensionPoint, Serializable { + @SuppressWarnings("unchecked") + default Descriptor getDescriptor() { + return (Descriptor) Jenkins.get().getDescriptorOrDie(getClass()); + } + + /** + * @param run The executing run that has one of its step requiring environment filters + * @param builder Normally inherits from {@link EnvVarsFilterableBuilder} but not forced to let reflection usage in plugins + * @param launcher The launcher that will be used to run the command + * @return true iff the rule can be applied to that builder + */ + boolean isApplicable(@CheckForNull Run run, @NonNull Object builder, @NonNull Launcher launcher); +} diff --git a/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterLocalRule.java b/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterLocalRule.java new file mode 100644 index 000000000000..eb31bab94275 --- /dev/null +++ b/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterLocalRule.java @@ -0,0 +1,47 @@ +/* + * 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 jenkins.tasks.filters; + +import hudson.ExtensionPoint; +import hudson.model.Describable; +import jenkins.model.Jenkins; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; + +import java.io.Serializable; + +/** + * Environment variables filter rule that is specific to a job configuration, using script-specific variables, etc.

+ * The job types can be filtered using {@link EnvVarsFilterLocalRuleDescriptor#isApplicable(Class)} + * + * The local rules are applied before the global ones. + * + * @since TODO + */ +@Restricted(Beta.class) +public interface EnvVarsFilterLocalRule extends Describable, EnvVarsFilterRule, ExtensionPoint, Serializable { + default EnvVarsFilterLocalRuleDescriptor getDescriptor() { + return (EnvVarsFilterLocalRuleDescriptor) Jenkins.get().getDescriptorOrDie(getClass()); + } +} diff --git a/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterLocalRuleDescriptor.java b/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterLocalRuleDescriptor.java new file mode 100644 index 000000000000..13812025831e --- /dev/null +++ b/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterLocalRuleDescriptor.java @@ -0,0 +1,55 @@ +/* + * 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 jenkins.tasks.filters; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.DescriptorExtensionList; +import hudson.model.Descriptor; +import jenkins.model.Jenkins; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Descriptor for the local rule. Compared to the global rule, it's the descriptor that determine + * if the rule is applicable to a given builder and then applied every time.

+ * For global rule it's the inverse, the rule itself determines when it's applicable. + * + * @since TODO + */ +@Restricted(Beta.class) +public abstract class EnvVarsFilterLocalRuleDescriptor extends Descriptor { + public abstract boolean isApplicable(@NonNull Class builderClass); + + public static List allApplicableFor(Class builderClass) { + DescriptorExtensionList allSpecificRules = + Jenkins.get().getDescriptorList(EnvVarsFilterLocalRule.class); + + return allSpecificRules.stream() + .filter(rule -> rule.isApplicable(builderClass)) + .collect(Collectors.toList()); + } +} diff --git a/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterRule.java b/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterRule.java new file mode 100644 index 000000000000..384021b81d81 --- /dev/null +++ b/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterRule.java @@ -0,0 +1,57 @@ +/* + * 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 jenkins.tasks.filters; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.EnvVars; +import hudson.Extension; +import hudson.model.Describable; +import hudson.model.Descriptor; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; + +import java.io.Serializable; + +/** + * The order of execution of the rules is determined by first their type (local before global) + * and then, by default, their {@link Extension#ordinal()}, higher ordinal first, but configuration can customize the order. + */ +@Restricted(Beta.class) +public interface EnvVarsFilterRule extends Serializable { + /** + * In case the filter detects something that must stop the build, it must throw a {@link EnvVarsFilterException}. + * This method may be executed on agents through a remoting channel. + */ + void filter(@NonNull EnvVars envVars, @NonNull EnvVarsFilterRuleContext context) throws EnvVarsFilterException; + + default String getDisplayName() { + if (this instanceof Describable) { + final Descriptor descriptor = ((Describable) this).getDescriptor(); + if (descriptor != null) { + return descriptor.getDisplayName(); + } + } + return this.getClass().getSimpleName(); + } +} diff --git a/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterRuleContext.java b/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterRuleContext.java new file mode 100644 index 000000000000..325d32f3be24 --- /dev/null +++ b/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterRuleContext.java @@ -0,0 +1,54 @@ +/* + * 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 jenkins.tasks.filters; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Launcher; +import hudson.model.TaskListener; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; + +/** + * Information that is used for the environment filtering process. + * + * @since TODO + */ +@Restricted(Beta.class) +public class EnvVarsFilterRuleContext { + private final Launcher launcher; + private final TaskListener taskListener; + + public EnvVarsFilterRuleContext(@NonNull Launcher launcher, @NonNull TaskListener taskListener) { + this.launcher = launcher; + this.taskListener = taskListener; + } + + public Launcher getLauncher() { + return launcher; + } + + public TaskListener getTaskListener() { + return taskListener; + } +} diff --git a/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterRuleWrapper.java b/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterRuleWrapper.java new file mode 100644 index 000000000000..72d83298667c --- /dev/null +++ b/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterRuleWrapper.java @@ -0,0 +1,83 @@ +/* + * 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 jenkins.tasks.filters; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.EnvVars; +import hudson.Launcher; +import hudson.model.Run; +import hudson.model.TaskListener; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Helper class that provide the list of rules (local + global) for a given builder. + * + * @since TODO + */ +@Restricted(NoExternalUse.class) +public class EnvVarsFilterRuleWrapper implements Serializable { + private static final long serialVersionUID = -8647970104978388598L; + private List rules; + + public EnvVarsFilterRuleWrapper(@NonNull List rules) { + this.rules = rules; + } + + public static @NonNull + EnvVarsFilterRuleWrapper createRuleWrapper(@CheckForNull Run run, + @NonNull Object builder, + @NonNull Launcher launcher, + @NonNull List localRules) { + List globalRules = EnvVarsFilterGlobalConfiguration.getAllActivatedGlobalRules(); + List applicableGlobalRules = globalRules.stream() + .filter(rule -> rule.isApplicable(run, builder, launcher)) + .collect(Collectors.toList()); + + List applicableRules = new ArrayList<>(); + applicableRules.addAll(localRules); + applicableRules.addAll(applicableGlobalRules); + + return new EnvVarsFilterRuleWrapper(applicableRules); + } + + public void filter(@NonNull EnvVars envVars, @NonNull Launcher launcher, @NonNull TaskListener listener) throws EnvVarsFilterException { + EnvVarsFilterRuleContext context = new EnvVarsFilterRuleContext(launcher, listener); + for (EnvVarsFilterRule rule : rules) { + try { + rule.filter(envVars, context); + } catch (EnvVarsFilterException e) { + String message = String.format("Environment variable filtering failed due to violation with the message: %s", e.getMessage()); + context.getTaskListener().error(message); + throw e; + } + } + } +} diff --git a/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterableBuilder.java b/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterableBuilder.java new file mode 100644 index 000000000000..001ead6a8460 --- /dev/null +++ b/core/src/main/java/jenkins/tasks/filters/EnvVarsFilterableBuilder.java @@ -0,0 +1,51 @@ +/* + * 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 jenkins.tasks.filters; + +import edu.umd.cs.findbugs.annotations.NonNull; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; + +import java.util.Collections; +import java.util.List; + +/** + * Builder step that wants to integrate local environment filter rules should implement this interface + * + * @since TODO + */ +@Restricted(Beta.class) +public interface EnvVarsFilterableBuilder { + /** + * The order is respected for the execution. Local rules will be executed before the global ones. + * + * This method is called only once per step to create the {@link EnvVarsFilterRuleContext}. + * + * The default implementation returns an empty list; this allows build steps to support global rules. + */ + @NonNull + default List buildEnvVarsFilterRules() { + return Collections.emptyList(); + } +} diff --git a/core/src/main/java/jenkins/tasks/filters/impl/RetainVariablesLocalRule.java b/core/src/main/java/jenkins/tasks/filters/impl/RetainVariablesLocalRule.java new file mode 100644 index 000000000000..c0fed8d4ea81 --- /dev/null +++ b/core/src/main/java/jenkins/tasks/filters/impl/RetainVariablesLocalRule.java @@ -0,0 +1,227 @@ +/* + * 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 jenkins.tasks.filters.impl; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.EnvVars; +import hudson.Extension; +import hudson.model.Job; +import hudson.model.Run; +import hudson.util.FormValidation; +import jenkins.tasks.filters.EnvVarsFilterRuleContext; +import jenkins.tasks.filters.EnvVarsFilterLocalRule; +import jenkins.tasks.filters.EnvVarsFilterLocalRuleDescriptor; +import jenkins.tasks.filters.EnvVarsFilterableBuilder; +import org.apache.commons.lang.StringUtils; +import org.jenkinsci.Symbol; +import org.jvnet.localizer.Localizable; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Local rule that removes all the non-retained variables for that step. + * + * @since TODO + */ +@Restricted(NoExternalUse.class) +public class RetainVariablesLocalRule implements EnvVarsFilterLocalRule { + + /** + * The variables considered to be 'characteristic' for the purposes of this rule. + * + * @see Job#getCharacteristicEnvVars() + * @see Run#getCharacteristicEnvVars() + */ + // TODO Make the 'HUDSON_COOKIE' variable less special so we can remove it. + // TODO consider just querying the build, if any, for its characteristic env vars + private static final List CHARACTERISTIC_ENV_VARS = Arrays.asList("jenkins_server_cookie", "hudson_server_cookie", "job_name", "job_base_name", "build_number", "build_id", "build_tag"); + /** + * List of lowercase names of variable that will be retained from removal + */ + private String variables = ""; + private boolean retainCharacteristicEnvVars = true; + private ProcessVariablesHandling processVariablesHandling = ProcessVariablesHandling.RESET; + + @DataBoundConstructor + public RetainVariablesLocalRule() { + } + + @DataBoundSetter + public void setVariables(@NonNull String variables) { + this.variables = variables; + } + + private static List convertStringToList(@NonNull String variablesCommaSeparated) { + String[] variablesArray = variablesCommaSeparated.split("\\s+"); + List variables = new ArrayList<>(); + for (String nameFragment : variablesArray) { + if (StringUtils.isNotBlank(nameFragment)) { + variables.add(nameFragment.toLowerCase(Locale.ENGLISH)); + } + } + + Collections.sort(variables); // TODO do we really want to sort this? + return variables; + } + + // for jelly view + @Restricted(NoExternalUse.class) + public @NonNull String getVariables() { + return variables; + } + + @DataBoundSetter + public void setRetainCharacteristicEnvVars(boolean retainCharacteristicEnvVars) { + this.retainCharacteristicEnvVars = retainCharacteristicEnvVars; + } + + /** + * Whether to retain characteristic environment variables. + * @return true if and only if to retain characteristic environment variables. + * + * @see Job#getCharacteristicEnvVars() + * @see Run#getCharacteristicEnvVars() + */ + public boolean isRetainCharacteristicEnvVars() { + return retainCharacteristicEnvVars; + } + + private List variablesToRetain() { + List vars = new ArrayList<>(convertStringToList(this.variables)); + if (isRetainCharacteristicEnvVars()) { + vars.addAll(CHARACTERISTIC_ENV_VARS); + } + return vars; + } + + @Override + public void filter(@NonNull EnvVars envVars, @NonNull EnvVarsFilterRuleContext context) { + Map systemEnvVars = EnvVars.masterEnvVars; + + final List variablesRemoved = new ArrayList<>(); + final List variablesReset = new ArrayList<>(); + final List variables = variablesToRetain(); + for (Iterator> iterator = envVars.entrySet().iterator(); iterator.hasNext(); ) { + Map.Entry entry = iterator.next(); + String variableName = entry.getKey(); + String variableValue = entry.getValue(); + + if (!variables.contains(variableName.toLowerCase(Locale.ENGLISH))) { + // systemEnvVars's keys are case insensitive + String systemValue = systemEnvVars.get(variableName); + + if (systemValue == null) { + variablesRemoved.add(variableName); + iterator.remove(); + } else { + switch (processVariablesHandling) { + case RESET: + if (!systemValue.equals(variableValue)) { + variablesReset.add(variableName); + } + break; + case REMOVE: + variablesRemoved.add(variableName); + iterator.remove(); + break; + } + } + } + } + + if (!variablesRemoved.isEmpty()) { + context.getTaskListener().getLogger().println(Messages.RetainVariablesLocalRule_RemovalMessage(getDescriptor().getDisplayName(), StringUtils.join(variablesRemoved.toArray(), ", "))); + } + if (!variablesReset.isEmpty()) { + // reset the variables using the initial value from System + variablesReset.forEach(variableName -> envVars.put(variableName, systemEnvVars.get(variableName))); + context.getTaskListener().getLogger().println(Messages.RetainVariablesLocalRule_ResetMessage(getDescriptor().getDisplayName(), StringUtils.join(variablesReset.toArray(), ", "))); + } + } + + public ProcessVariablesHandling getProcessVariablesHandling() { + return processVariablesHandling; + } + + @DataBoundSetter + public void setProcessVariablesHandling(ProcessVariablesHandling processVariablesHandling) { + this.processVariablesHandling = processVariablesHandling; + } + + // the ordinal is used to sort the rules in term of execution, the higher value first + // and take care of the fact that local rules are always applied before global ones + @Extension(ordinal = 1000) + @Symbol("retainOnlyVariables") + public static final class DescriptorImpl extends EnvVarsFilterLocalRuleDescriptor { + + public DescriptorImpl() { + super(); + load(); + } + + @Restricted(NoExternalUse.class) + public FormValidation doCheckRetainCharacteristicEnvVars(@QueryParameter boolean value) { + if (!value) { + return FormValidation.warning(Messages.RetainVariablesLocalRule_CharacteristicEnvVarsFormValidationWarning()); + } + return FormValidation.ok(Messages.RetainVariablesLocalRule_CharacteristicEnvVarsFormValidationOK()); + } + + @Override + public @NonNull String getDisplayName() { + return Messages.RetainVariablesLocalRule_DisplayName(); + } + + @Override + public boolean isApplicable(@NonNull Class builderClass) { + return true; + } + } + + public enum ProcessVariablesHandling { + RESET(Messages._RetainVariablesLocalRule_RESET_DisplayName()), + REMOVE(Messages._RetainVariablesLocalRule_REMOVE_DisplayName()); + + private final Localizable localizable; + + ProcessVariablesHandling(Localizable localizable) { + this.localizable = localizable; + } + + public String getDisplayName() { + return localizable.toString(); + } + } +} diff --git a/core/src/main/resources/hudson/tasks/BatchFile/config.jelly b/core/src/main/resources/hudson/tasks/BatchFile/config.jelly index 19fbdcef646d..4918b5501270 100644 --- a/core/src/main/resources/hudson/tasks/BatchFile/config.jelly +++ b/core/src/main/resources/hudson/tasks/BatchFile/config.jelly @@ -32,5 +32,17 @@ THE SOFTWARE. + + + + + + diff --git a/core/src/main/resources/hudson/tasks/BatchFile/config.properties b/core/src/main/resources/hudson/tasks/BatchFile/config.properties index e416300f208f..d88d5ac12011 100644 --- a/core/src/main/resources/hudson/tasks/BatchFile/config.properties +++ b/core/src/main/resources/hudson/tasks/BatchFile/config.properties @@ -21,3 +21,5 @@ # THE SOFTWARE. description=See the list of available environment variables +filterRules=Environment filters +addFilterRule=Add environment filter diff --git a/core/src/main/resources/hudson/tasks/Shell/config.groovy b/core/src/main/resources/hudson/tasks/Shell/config.groovy index 811ca08d1d72..f895ffe88f3e 100644 --- a/core/src/main/resources/hudson/tasks/Shell/config.groovy +++ b/core/src/main/resources/hudson/tasks/Shell/config.groovy @@ -33,4 +33,17 @@ f.advanced() { f.number(clazz:"positive-number", value: instance?.unstableReturn, min:1, max:255, step:1) } + if (instance?.configuredLocalRules || descriptor.applicableLocalRules) { + f.entry(title: _("filterRules")) { + f.hetero_list( + name: "configuredLocalRules", + hasHeader: true, + oneEach: true, + disableDragAndDrop: true, + descriptors: descriptor.applicableLocalRules, + items: instance?.configuredLocalRules, + addCaption: _("addFilterRule") + ) + } + } } diff --git a/core/src/main/resources/hudson/tasks/Shell/config.properties b/core/src/main/resources/hudson/tasks/Shell/config.properties index 0d7b36f4a5a1..5a69c4dc7c2e 100644 --- a/core/src/main/resources/hudson/tasks/Shell/config.properties +++ b/core/src/main/resources/hudson/tasks/Shell/config.properties @@ -21,3 +21,5 @@ # THE SOFTWARE. description=See the list of available environment variables +filterRules=Environment filters +addFilterRule=Add environment filter diff --git a/core/src/main/resources/jenkins/tasks/filters/EnvVarsFilterGlobalConfiguration/config.jelly b/core/src/main/resources/jenkins/tasks/filters/EnvVarsFilterGlobalConfiguration/config.jelly new file mode 100644 index 000000000000..04af1021f5b1 --- /dev/null +++ b/core/src/main/resources/jenkins/tasks/filters/EnvVarsFilterGlobalConfiguration/config.jelly @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + diff --git a/core/src/main/resources/jenkins/tasks/filters/EnvVarsFilterGlobalConfiguration/config.properties b/core/src/main/resources/jenkins/tasks/filters/EnvVarsFilterGlobalConfiguration/config.properties new file mode 100644 index 000000000000..5fc3224a81a8 --- /dev/null +++ b/core/src/main/resources/jenkins/tasks/filters/EnvVarsFilterGlobalConfiguration/config.properties @@ -0,0 +1,25 @@ +# 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. +jobEnvironmentVariableFilters=Filter build step environment variables +globalRules=Global filter rules +showGlobalRules=Show global filter rules +addRule=Add filter rule diff --git a/core/src/main/resources/jenkins/tasks/filters/impl/Messages.properties b/core/src/main/resources/jenkins/tasks/filters/impl/Messages.properties new file mode 100644 index 000000000000..b022f01cfe70 --- /dev/null +++ b/core/src/main/resources/jenkins/tasks/filters/impl/Messages.properties @@ -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. + +RetainVariablesLocalRule.DisplayName=Only Keep Specified Environment Variables +RetainVariablesLocalRule_RESET_DisplayName=Reset to default value +RetainVariablesLocalRule_REMOVE_DisplayName=Remove from environment +RetainVariablesLocalRule.RemovalMessage=The following environment variables were removed by ''{0}'': {1} +RetainVariablesLocalRule.ResetMessage=The following environment variables were reset to their system default value by ''{0}'': {1} +RetainVariablesLocalRule.CharacteristicEnvVarsFormValidationWarning=It is recommended to retain characteristic environment variables, because Jenkins uses them to identify and kill runaway processes after a build is finished. +RetainVariablesLocalRule.CharacteristicEnvVarsFormValidationOK=In addition to any environment variables listed above, Jenkins will also retain environment variables it needs to identify and kill runaway processes when the build is done. diff --git a/core/src/main/resources/jenkins/tasks/filters/impl/RetainVariablesLocalRule/config.jelly b/core/src/main/resources/jenkins/tasks/filters/impl/RetainVariablesLocalRule/config.jelly new file mode 100644 index 000000000000..76a418690f8d --- /dev/null +++ b/core/src/main/resources/jenkins/tasks/filters/impl/RetainVariablesLocalRule/config.jelly @@ -0,0 +1,39 @@ + + + + + + + + + + + + + ${it.displayName} + + + diff --git a/core/src/main/resources/jenkins/tasks/filters/impl/RetainVariablesLocalRule/config.properties b/core/src/main/resources/jenkins/tasks/filters/impl/RetainVariablesLocalRule/config.properties new file mode 100644 index 000000000000..430c740d6484 --- /dev/null +++ b/core/src/main/resources/jenkins/tasks/filters/impl/RetainVariablesLocalRule/config.properties @@ -0,0 +1,26 @@ +# 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. +variablesLabel=Environment variables to retain +variablesDescription=All other environment variables provided by Jenkins will be removed. \ + Environment variables defined outside Jenkins will be reset to their default value or removed, depending on the option ''Process environment variables handling'', unless specified here. +retainCharacteristicEnvVarsLabel=Retain characteristic environment variables +retainProcessVariablesLabel=Process environment variables handling diff --git a/core/src/main/resources/jenkins/tasks/filters/impl/RetainVariablesLocalRule/help-retainCharacteristicEnvVars.html b/core/src/main/resources/jenkins/tasks/filters/impl/RetainVariablesLocalRule/help-retainCharacteristicEnvVars.html new file mode 100644 index 000000000000..774d44c499f0 --- /dev/null +++ b/core/src/main/resources/jenkins/tasks/filters/impl/RetainVariablesLocalRule/help-retainCharacteristicEnvVars.html @@ -0,0 +1,5 @@ +

+ When checked, characteristic environment variables will be retained in addition to the variables listed above. + These environment variables are job- and build-specific, defined by Jenkins, and are used to identify and kill processes started by this build step. + See the documentation for more details on starting processes. +

diff --git a/core/src/main/resources/jenkins/tasks/filters/impl/RetainVariablesLocalRule/help-variables.html b/core/src/main/resources/jenkins/tasks/filters/impl/RetainVariablesLocalRule/help-variables.html new file mode 100644 index 000000000000..0788d14647d3 --- /dev/null +++ b/core/src/main/resources/jenkins/tasks/filters/impl/RetainVariablesLocalRule/help-variables.html @@ -0,0 +1,3 @@ +
+

Whitespace separated, case insensitive list of environment variables that will be retained, i.e. not removed from the environment of this build step or reset to their default.

+
diff --git a/core/src/main/resources/jenkins/tasks/filters/impl/RetainVariablesLocalRule/help.html b/core/src/main/resources/jenkins/tasks/filters/impl/RetainVariablesLocalRule/help.html new file mode 100644 index 000000000000..2a4c6fb57117 --- /dev/null +++ b/core/src/main/resources/jenkins/tasks/filters/impl/RetainVariablesLocalRule/help.html @@ -0,0 +1,48 @@ +
+

Limit which environment variables are passed to a build step.

+ +

Environment variables passed to the build step are filtered, unless listed below.

+ +

The behavior of this filter depends on whether the environment variable is originally defined outside Jenkins:

+
    +
  • If the environment variable originates from Jenkins configuration, such as JOB_URL, + it will not be passed to the build step unless specified here.
  • +
  • If the environment variable originates from outside Jenkins, such as PATH, + the behavior depends on the option Process environment variables handling: + If that option is set to Retain, the original value will be passed to the build step, discarding any modifications inside Jenkins. + If that option is set to Remove, the variable will not be passed to the build step. +
  • +
+

+ The following table shows the effect of filtering on an environment variable: +

+ + + + + + + + + + + + + + + + +
BehaviorOriginally defined outside JenkinsOriginally defined inside Jenkins
+ Process environment variables handling: reset + + Variable is reset to original value + + Variable is removed +
+ Process environment variables handling: removed + + Variable is removed + + Variable is removed +
+
diff --git a/core/src/main/resources/lib/form/hetero-list.jelly b/core/src/main/resources/lib/form/hetero-list.jelly index 973fdd4cb4a9..bf26d1414333 100644 --- a/core/src/main/resources/lib/form/hetero-list.jelly +++ b/core/src/main/resources/lib/form/hetero-list.jelly @@ -73,6 +73,10 @@ THE SOFTWARE. If set to an item of the form className#classMethod, it will be used to call className.classMethod(descriptor) to calculate each item title. + + If true the drag and drop will not be activated. This just removes the drag and + drop UI, it will not prevent users from manually submitting a different order. + @@ -81,7 +85,7 @@ THE SOFTWARE. -
+
${descriptor.displayName}
@@ -112,11 +116,11 @@ THE SOFTWARE.
- +
- +
@@ -143,7 +147,7 @@ THE SOFTWARE.
- + diff --git a/test/src/test/java/jenkins/tasks/filters/impl/RetainVariablesLocalRuleTest.java b/test/src/test/java/jenkins/tasks/filters/impl/RetainVariablesLocalRuleTest.java new file mode 100644 index 000000000000..b2bc601842ce --- /dev/null +++ b/test/src/test/java/jenkins/tasks/filters/impl/RetainVariablesLocalRuleTest.java @@ -0,0 +1,391 @@ +/* + * 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 jenkins.tasks.filters.impl; + +import hudson.Functions; +import hudson.model.Build; +import hudson.model.Cause; +import hudson.model.FreeStyleBuild; +import hudson.model.FreeStyleProject; +import hudson.model.ParametersAction; +import hudson.model.ParametersDefinitionProperty; +import hudson.model.Result; +import hudson.model.StringParameterDefinition; +import hudson.model.StringParameterValue; +import hudson.tasks.BatchFile; +import hudson.tasks.Shell; +import jenkins.tasks.filters.EnvVarsFilterGlobalConfiguration; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; + +public class RetainVariablesLocalRuleTest { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Test + public void retainVariable_removeUnwantedVariables_batch() throws Exception { + assumeTrue(Functions.isWindows()); + + FreeStyleProject p = j.createFreeStyleProject(); + BatchFile batch = new BatchFile("echo \"begin %what% %who% end\""); + p.getBuildersList().add(batch); + p.addProperty(new ParametersDefinitionProperty( + new StringParameterDefinition("what", "Hello"), + new StringParameterDefinition("who", "World") + )); + + {// the rule allows the user to retain only a subset of variable + RetainVariablesLocalRule localRule = new RetainVariablesLocalRule(); + localRule.setVariables("what"); + batch.setConfiguredLocalRules(Collections.singletonList(localRule)); + + FreeStyleBuild build = j.assertBuildStatus(Result.SUCCESS, p.scheduleBuild2(0, (Cause) null, new ParametersAction( + new StringParameterValue("what", "hello"), + new StringParameterValue("who", "world") + ))); + + assertContainsSequentially(build, "begin hello end"); + assertDoesNotContainsSequentially(build, "world"); + } + + {// the rule allows the user to retain only a subset of variable (second example) + RetainVariablesLocalRule localRule = new RetainVariablesLocalRule(); + localRule.setVariables("who"); + batch.setConfiguredLocalRules(Collections.singletonList(localRule)); + + FreeStyleBuild build = j.assertBuildStatus(Result.SUCCESS, p.scheduleBuild2(0, (Cause) null, new ParametersAction( + new StringParameterValue("what", "hello"), + new StringParameterValue("who", "world") + ))); + + assertContainsSequentially(build, "begin world end"); + assertDoesNotContainsSequentially(build, "hello"); + } + } + + @Test + public void retainVariable_removeModifiedSystemEnv_batch() throws Exception { + assumeTrue(Functions.isWindows()); + + FreeStyleProject p = j.createFreeStyleProject(); + BatchFile batch = new BatchFile("echo \"begin %what% [=[%path%]=] end\""); + p.getBuildersList().add(batch); + p.addProperty(new ParametersDefinitionProperty( + new StringParameterDefinition("what", "Hello"), + // override the System env variable + new StringParameterDefinition("path", null) + )); + + String initialValueOfPath; + + {// no attempt to modify path (except other plugin) + RetainVariablesLocalRule localRule = new RetainVariablesLocalRule(); + localRule.setVariables("what"); + batch.setConfiguredLocalRules(Collections.singletonList(localRule)); + + FreeStyleBuild build = j.assertBuildStatus(Result.SUCCESS, p.scheduleBuild2(0, (Cause) null, new ParametersAction( + new StringParameterValue("what", "hello") + ))); + + initialValueOfPath = findStringEnclosedBy(build, "[=[", "]=]"); + assertContainsSequentially(build, "hello"); + } + + {// does not accept modification of path + RetainVariablesLocalRule localRule = new RetainVariablesLocalRule(); + localRule.setVariables("what"); + batch.setConfiguredLocalRules(Collections.singletonList(localRule)); + + FreeStyleBuild build = j.assertBuildStatus(Result.SUCCESS, p.scheduleBuild2(0, (Cause) null, new ParametersAction( + new StringParameterValue("what", "hello"), + new StringParameterValue("path", "modificationOfPath") + ))); + + // potentially plugins modified the path also + assertContainsSequentially(build, "begin hello"); + assertContainsSequentially(build, initialValueOfPath); + assertDoesNotContainsSequentially(build, "modificationOfPath"); + } + + {// accept modification of path + RetainVariablesLocalRule localRule = new RetainVariablesLocalRule(); + localRule.setVariables("what path"); + batch.setConfiguredLocalRules(Collections.singletonList(localRule)); + + FreeStyleBuild build = j.assertBuildStatus(Result.SUCCESS, p.scheduleBuild2(0, (Cause) null, new ParametersAction( + new StringParameterValue("what", "hello"), + new StringParameterValue("path", "modificationOfPath;$PATH") + ))); + + // potentially plugins modified the path also + assertContainsSequentially(build, "begin hello"); + assertContainsSequentially(build, "modificationOfPath"); + } + } + + @Test + public void retainVariable_removeModifiedSystemEnv_shell() throws Exception { + assumeFalse(Functions.isWindows()); + + FreeStyleProject p = j.createFreeStyleProject(); + Shell batch = new Shell("echo \"begin $what [=[$PATH]=] end\""); + p.getBuildersList().add(batch); + p.addProperty(new ParametersDefinitionProperty( + new StringParameterDefinition("what", "Hello"), + // override the System env variable + new StringParameterDefinition("path", null) + )); + + String initialValueOfPath; + + {// no attempt to modify path (except other plugin) + RetainVariablesLocalRule localRule = new RetainVariablesLocalRule(); + localRule.setVariables("what"); + batch.setConfiguredLocalRules(Collections.singletonList(localRule)); + + FreeStyleBuild build = j.assertBuildStatus(Result.SUCCESS, p.scheduleBuild2(0, (Cause) null, new ParametersAction( + new StringParameterValue("what", "hello") + ))); + + initialValueOfPath = findStringEnclosedBy(build, "[=[", "]=]"); + assertContainsSequentially(build, "hello"); + } + + {// does not accept modification of path + RetainVariablesLocalRule localRule = new RetainVariablesLocalRule(); + localRule.setVariables("what"); + batch.setConfiguredLocalRules(Collections.singletonList(localRule)); + + FreeStyleBuild build = j.assertBuildStatus(Result.SUCCESS, p.scheduleBuild2(0, (Cause) null, new ParametersAction( + new StringParameterValue("what", "hello"), + new StringParameterValue("path", "modificationOfPath") + ))); + + // potentially plugins modified the path also + assertContainsSequentially(build, "begin hello"); + assertContainsSequentially(build, initialValueOfPath); + assertDoesNotContainsSequentially(build, "modificationOfPath"); + } + + {// accept modification of path + RetainVariablesLocalRule localRule = new RetainVariablesLocalRule(); + localRule.setVariables("what path"); + batch.setConfiguredLocalRules(Collections.singletonList(localRule)); + + FreeStyleBuild build = j.assertBuildStatus(Result.SUCCESS, p.scheduleBuild2(0, (Cause) null, new ParametersAction( + new StringParameterValue("what", "hello"), + new StringParameterValue("path", "modificationOfPath;$PATH") + ))); + + // potentially plugins modified the path also + assertContainsSequentially(build, "begin hello"); + assertContainsSequentially(build, "modificationOfPath"); + } + } + + @Test + public void retainVariable_removeUnwantedVariables_shell() throws Exception { + assumeFalse(Functions.isWindows()); + + FreeStyleProject p = j.createFreeStyleProject(); + Shell shell = new Shell("echo \"begin $what $who end\""); + p.getBuildersList().add(shell); + p.addProperty(new ParametersDefinitionProperty( + new StringParameterDefinition("what", "Hello"), + new StringParameterDefinition("who", "World") + )); + + {// the rule allows the user to retain only a subset of variable + RetainVariablesLocalRule localRule = new RetainVariablesLocalRule(); + localRule.setVariables("what"); + shell.setConfiguredLocalRules(Collections.singletonList(localRule)); + + FreeStyleBuild build = j.assertBuildStatus(Result.SUCCESS, p.scheduleBuild2(0, (Cause) null, new ParametersAction( + new StringParameterValue("what", "hello"), + new StringParameterValue("who", "world") + ))); + + assertContainsSequentially(build, "begin hello end"); + assertDoesNotContainsSequentially(build, "world"); + } + } + + @Test + public void retainVariable_removeSystemVariables_shell() throws Exception { + assumeFalse(Functions.isWindows()); + + FreeStyleProject p = j.createFreeStyleProject(); + Shell shell = new Shell("env"); + p.getBuildersList().add(shell); + + FreeStyleBuild build = j.assertBuildStatus(Result.SUCCESS, p.scheduleBuild2(0, (Cause) null)); + List unfilteredLogOutput = build.getLog(200).stream().filter(s -> s.contains("=")).map(s -> s.substring(0, s.indexOf('='))).collect(Collectors.toList()); + + p.getBuildersList().remove(shell); + + Shell filteredShell = new Shell("env"); + + RetainVariablesLocalRule localRule = new RetainVariablesLocalRule(); + localRule.setVariables("path"); // seems to work without but may be env dependent + localRule.setRetainCharacteristicEnvVars(false); + localRule.setProcessVariablesHandling(RetainVariablesLocalRule.ProcessVariablesHandling.REMOVE); + filteredShell.setConfiguredLocalRules(Collections.singletonList(localRule)); + p.getBuildersList().add(filteredShell); + + build = j.assertBuildStatus(Result.SUCCESS, p.scheduleBuild2(0, (Cause) null)); + List filteredLogOutput = build.getLog(200).stream().filter(s -> s.contains("=")).map(s -> s.substring(0, s.indexOf('='))).collect(Collectors.toList()); + + assertTrue(filteredLogOutput.size() < unfilteredLogOutput.size() - 10); // 10 is a value slightly larger than the number of characteristic env vars (7) + List filteredButNotUnfiltered = new ArrayList<>(filteredLogOutput); + filteredButNotUnfiltered.removeAll(unfilteredLogOutput); + assertFalse(filteredLogOutput.contains("HOME")); + assertFalse(filteredLogOutput.contains("USER")); + assertFalse(filteredLogOutput.contains("JENKINS_HOME")); + assertFalse(filteredLogOutput.contains("")); + } + + @Test + public void multipleBuildSteps_haveSeparateRules_batch() throws Exception { + assumeTrue(Functions.isWindows()); + + FreeStyleProject p = j.createFreeStyleProject(); + BatchFile batch1 = new BatchFile("echo \"Step1: %what% %who%\""); + BatchFile batch2 = new BatchFile("echo \"Step2: %what% %who%\""); + p.getBuildersList().add(batch1); + p.getBuildersList().add(batch2); + p.addProperty(new ParametersDefinitionProperty( + new StringParameterDefinition("what", "Hello"), + new StringParameterDefinition("who", "World") + )); + + {// two steps with a specified local rule on each, there is not interaction + RetainVariablesLocalRule localRule1 = new RetainVariablesLocalRule(); + // take care to allow the PATH to be used, without that the cmd is not found + localRule1.setVariables("what"); + batch1.setConfiguredLocalRules(Collections.singletonList(localRule1)); + + RetainVariablesLocalRule localRule2 = new RetainVariablesLocalRule(); + // take care to allow the PATH to be used, without that the cmd is not found + localRule2.setVariables("who"); + batch2.setConfiguredLocalRules(Collections.singletonList(localRule2)); + + FreeStyleBuild build = j.assertBuildStatus(Result.SUCCESS, p.scheduleBuild2(0, (Cause) null, new ParametersAction( + new StringParameterValue("what", "hello"), + new StringParameterValue("who", "world") + ))); + + assertContainsSequentially(build, "Step1: hello"); + // due to the display of each command, the log displays `echo "Step2: world"`, then on the next line the result + assertDoesNotContainsSequentially(build, "world", "Step2:", "world"); + assertContainsSequentially(build, "Step2: world"); + } + } + + @Test + public void multipleBuildSteps_haveSeparateRules_shell() throws Exception { + assumeFalse(Functions.isWindows()); + + FreeStyleProject p = j.createFreeStyleProject(); + Shell batch1 = new Shell("echo \"Step1: $what $who\""); + Shell batch2 = new Shell("echo \"Step2: $what $who\""); + p.getBuildersList().add(batch1); + p.getBuildersList().add(batch2); + p.addProperty(new ParametersDefinitionProperty( + new StringParameterDefinition("what", "Hello"), + new StringParameterDefinition("who", "World") + )); + + {// two steps with a specified local rule on each, there is not interaction + RetainVariablesLocalRule localRule1 = new RetainVariablesLocalRule(); + // take care to allow the PATH to be used, without that the cmd is not found + localRule1.setVariables("what"); + batch1.setConfiguredLocalRules(Collections.singletonList(localRule1)); + + RetainVariablesLocalRule localRule2 = new RetainVariablesLocalRule(); + // take care to allow the PATH to be used, without that the cmd is not found + localRule2.setVariables("who"); + batch2.setConfiguredLocalRules(Collections.singletonList(localRule2)); + + FreeStyleBuild build = j.assertBuildStatus(Result.SUCCESS, p.scheduleBuild2(0, (Cause) null, new ParametersAction( + new StringParameterValue("what", "hello"), + new StringParameterValue("who", "world") + ))); + + assertContainsSequentially(build, "Step1: hello"); + // due to the display of each command, the log displays `echo "Step2: world"`, then on the next line the result + assertDoesNotContainsSequentially(build, "world", "Step2:", "world"); + assertContainsSequentially(build, "Step2: world"); + } + } + + private void assertContainsSequentially(Build build, String... values) throws Exception { + int i = 0; + for (String line : build.getLog(128)) { + if (line.contains(values[i])) { + i++; + if (i >= values.length) { + return; + } + } + } + fail("Does not contains the value: " + values[i]); + } + + private String findStringEnclosedBy(Build build, String before, String after) throws Exception { + for (String line : build.getLog(128)) { + int beforeIndex = line.indexOf(before); + int afterIndex = line.indexOf(after, beforeIndex + before.length()); + if (beforeIndex != -1 && afterIndex != -1) { + return line.substring(beforeIndex + before.length(), afterIndex); + } + } + return ""; + } + + private void assertDoesNotContainsSequentially(Build build, String... values) throws Exception { + int i = 0; + for (String line : build.getLog(128)) { + if (line.contains(values[i])) { + i++; + if (i >= values.length) { + fail("Does contains all the values"); + return; + } + } + } + } +}