diff --git a/src/main/java/org/jenkinsci/plugins/workflow/cps/replay/ReplayPipelineCommand.java b/src/main/java/org/jenkinsci/plugins/workflow/cps/replay/ReplayPipelineCommand.java index 7e8e3c7ab..b165bccc2 100644 --- a/src/main/java/org/jenkinsci/plugins/workflow/cps/replay/ReplayPipelineCommand.java +++ b/src/main/java/org/jenkinsci/plugins/workflow/cps/replay/ReplayPipelineCommand.java @@ -30,16 +30,23 @@ import hudson.cli.handlers.GenericItemOptionHandler; import hudson.model.Job; import hudson.model.Run; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.util.AbstractMap.SimpleEntry; +import java.util.ArrayList; import java.util.HashMap; import java.util.Map; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.io.IOUtils; -import org.jenkinsci.plugins.workflow.cps.replay.Messages; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.Option; import org.kohsuke.args4j.OptionDef; import org.kohsuke.args4j.spi.Setter; + @Extension public class ReplayPipelineCommand extends CLICommand { @Argument(required=true, index=0, metaVar="JOB", usage="Name of the job to replay.", handler=JobHandler.class) @@ -51,6 +58,9 @@ @Option(name="-s", aliases="--script", metaVar="SCRIPT", usage="Name of script to edit, such as Script3, if not the main Jenkinsfile.") public String script; + @Option(name="-S", aliases="--scripts", usage="Pass tarball of override scripts to override build scripts on remote") + private boolean scripts; + @Override public String getShortDescription() { return Messages.ReplayCommand_shortDescription(); } @@ -70,20 +80,80 @@ if (!action.isEnabled()) { throw new AbortException("Not authorized to replay builds of this job"); } - String text = IOUtils.toString(stdin); - if (script != null) { - Map replacementLoadedScripts = new HashMap(action.getOriginalLoadedScripts()); - if (!replacementLoadedScripts.containsKey(script)) { - throw new AbortException("Unrecognized script name among " + replacementLoadedScripts.keySet()); + if (scripts) { + ArrayList> replacementScripts = parseScripts(stdin); + Map replacementLoadedScripts = new HashMap<>(action.getOriginalLoadedScripts()); + Boolean overrideJenkinsfile = false; + String jenkinsfileContent = ""; + + for (SimpleEntry pair : replacementScripts) { + String scriptName = pair.getKey(); + String scriptContent = pair.getValue(); + + if (scriptName.equals("Jenkinsfile")) { + overrideJenkinsfile = true; + jenkinsfileContent = scriptContent; + } else { + if (!replacementLoadedScripts.containsKey(scriptName)) { + throw new AbortException("Unrecognized script name " + scriptName + " among " + replacementLoadedScripts.keySet()); + } + replacementLoadedScripts.put(scriptName, scriptContent); + } } - replacementLoadedScripts.put(script, text); - action.run(action.getOriginalScript(), replacementLoadedScripts); + + if (!overrideJenkinsfile) + jenkinsfileContent = action.getOriginalScript(); + + action.run(jenkinsfileContent, replacementLoadedScripts); } else { - action.run(text, action.getOriginalLoadedScripts()); + String text = IOUtils.toString(stdin); + if (script != null) { + Map replacementLoadedScripts = new HashMap(action.getOriginalLoadedScripts()); + if (!replacementLoadedScripts.containsKey(script)) { + throw new AbortException("Unrecognized script name among " + replacementLoadedScripts.keySet()); + } + replacementLoadedScripts.put(script, text); + action.run(action.getOriginalScript(), replacementLoadedScripts); + } else { + action.run(text, action.getOriginalLoadedScripts()); + } } return 0; } + protected ArrayList> parseScripts(InputStream tarInputStream) throws Exception { + ArrayList> list = new ArrayList<>(); + + try ( TarArchiveInputStream tarIn = new TarArchiveInputStream(tarInputStream) ) { + TarArchiveEntry tarEntry = tarIn.getNextTarEntry(); + + while (tarEntry != null) { + String scriptName = tarEntry.getName(); + + int BUFFER_SIZE = 512; + int count; + byte data[] = new byte[BUFFER_SIZE]; + + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + BufferedOutputStream outStream = new BufferedOutputStream(byteStream, BUFFER_SIZE); + while ((count = tarIn.read(data, 0, BUFFER_SIZE)) != -1) { + outStream.write(data, 0, count); + } + outStream.flush(); + String scriptContent = byteStream.toString("UTF-8"); + + SimpleEntry entry = new SimpleEntry<>(scriptName, scriptContent); + list.add(entry); + + tarEntry = tarIn.getNextTarEntry(); + } + } catch ( Exception e) { + throw e; + } + + return list; + } + @SuppressWarnings("rawtypes") public static class JobHandler extends GenericItemOptionHandler { diff --git a/src/test/java/org/jenkinsci/plugins/workflow/cps/replay/ReplayActionTest.java b/src/test/java/org/jenkinsci/plugins/workflow/cps/replay/ReplayActionTest.java index 957a9fd81..c5de75f71 100644 --- a/src/test/java/org/jenkinsci/plugins/workflow/cps/replay/ReplayActionTest.java +++ b/src/test/java/org/jenkinsci/plugins/workflow/cps/replay/ReplayActionTest.java @@ -45,6 +45,7 @@ import hudson.security.GlobalMatrixAuthorizationStrategy; import hudson.security.Permission; import java.io.File; +import java.io.InputStream; import java.util.Collections; import java.util.List; import jenkins.model.Jenkins; @@ -287,6 +288,26 @@ private static boolean canRebuild(WorkflowRun b, String user) { }); } + @Test public void cliMultipleFiles() throws Exception { + story.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p"); + // As in loadStep, will set up a main and auxiliary script. + FilePath f = story.j.jenkins.getWorkspaceFor(p).child("f.groovy"); + f.write("'original text'", null); + p.setDefinition(new CpsFlowDefinition("node {def t = load 'f.groovy'; echo \"got ${t}\"}", true)); + WorkflowRun b1 = story.j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + story.j.assertLogContains("got original text", b1); + InputStream tarStream = this.getClass().getResourceAsStream("scripts.tar"); + assertEquals(0, new CLICommandInvoker(story.j, "replay-pipeline").withStdin(tarStream).invokeWithArgs("p", "-S").returnCode()); + story.j.waitUntilNoActivity(); + WorkflowRun b2 = p.getLastBuild(); + assertEquals(2, b2.getNumber()); + story.j.assertLogContains("received new text", b2); + } + }); + } + @Issue("JENKINS-47339") @Test public void rebuild() throws Exception { diff --git a/src/test/resources/org/jenkinsci/plugins/workflow/cps/replay/Jenkinsfile b/src/test/resources/org/jenkinsci/plugins/workflow/cps/replay/Jenkinsfile new file mode 100644 index 000000000..dca415e51 --- /dev/null +++ b/src/test/resources/org/jenkinsci/plugins/workflow/cps/replay/Jenkinsfile @@ -0,0 +1,4 @@ +node { + def t = load 'f.groovy' + echo "received ${t}" +} diff --git a/src/test/resources/org/jenkinsci/plugins/workflow/cps/replay/Script1 b/src/test/resources/org/jenkinsci/plugins/workflow/cps/replay/Script1 new file mode 100644 index 000000000..16587c596 --- /dev/null +++ b/src/test/resources/org/jenkinsci/plugins/workflow/cps/replay/Script1 @@ -0,0 +1 @@ +'new text' diff --git a/src/test/resources/org/jenkinsci/plugins/workflow/cps/replay/scripts.tar b/src/test/resources/org/jenkinsci/plugins/workflow/cps/replay/scripts.tar new file mode 100644 index 000000000..7b5814ac0 Binary files /dev/null and b/src/test/resources/org/jenkinsci/plugins/workflow/cps/replay/scripts.tar differ