diff --git a/src/main/java/org/csanchez/jenkins/plugins/kubernetes/KubernetesSlave.java b/src/main/java/org/csanchez/jenkins/plugins/kubernetes/KubernetesSlave.java index d39a750af0..289bef5a8c 100644 --- a/src/main/java/org/csanchez/jenkins/plugins/kubernetes/KubernetesSlave.java +++ b/src/main/java/org/csanchez/jenkins/plugins/kubernetes/KubernetesSlave.java @@ -6,6 +6,7 @@ import java.security.UnrecoverableKeyException; import java.security.cert.CertificateEncodingException; import java.util.HashSet; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -166,6 +167,10 @@ public Cloud getCloud() { return Jenkins.getInstance().getCloud(getCloudName()); } + public Optional getPod() { + return pod == null ? Optional.empty() : Optional.of(pod); + } + /** * Returns the cloud instance which created this agent. * @return the cloud instance which created this agent. diff --git a/src/main/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/ContainerExecDecorator.java b/src/main/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/ContainerExecDecorator.java index 6be47ddf34..a89cd409e3 100755 --- a/src/main/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/ContainerExecDecorator.java +++ b/src/main/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/ContainerExecDecorator.java @@ -25,19 +25,24 @@ import java.io.PrintStream; import java.io.Serializable; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; - +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import io.fabric8.kubernetes.api.model.Container; import org.apache.commons.io.output.NullOutputStream; import org.apache.commons.io.output.TeeOutputStream; +import org.csanchez.jenkins.plugins.kubernetes.ContainerTemplate; +import org.csanchez.jenkins.plugins.kubernetes.KubernetesSlave; import org.jenkinsci.plugins.workflow.steps.EnvironmentExpander; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -240,29 +245,91 @@ public Launcher decorate(final Launcher launcher, final Node node) { @Override public Proc launch(ProcStarter starter) throws IOException { LOGGER.log(Level.FINEST, "Launch proc with environment: {0}", Arrays.toString(starter.envs())); + + // find container working dir + KubernetesSlave slave = (KubernetesSlave) node; + FilePath containerWorkingDirFilePath = starter.pwd(); + String containerWorkingDirFilePathStr = containerWorkingDirFilePath != null + ? containerWorkingDirFilePath.getRemote() : ContainerTemplate.DEFAULT_WORKING_DIR; + String containerWorkingDirStr = ContainerTemplate.DEFAULT_WORKING_DIR; + if (slave != null && slave.getPod().isPresent() && containerName != null) { + Optional container = slave.getPod().get().getSpec().getContainers().stream() + .filter(container1 -> container1.getName().equals(containerName)) + .findAny(); + Optional containerWorkingDir = Optional.empty(); + if (container.isPresent() && container.get().getWorkingDir() != null) { + containerWorkingDir = Optional.of(container.get().getWorkingDir()); + } + if (containerWorkingDir.isPresent()) { + containerWorkingDirStr = containerWorkingDir.get(); + } + + if (containerWorkingDir.isPresent() && ! containerWorkingDirFilePath.getRemote().startsWith(containerWorkingDirStr)) { + // Container has a custom workingDir, updated the pwd to match container working dir + containerWorkingDirFilePathStr = containerWorkingDirFilePath.getRemote().replaceFirst( + ContainerTemplate.DEFAULT_WORKING_DIR, containerWorkingDirStr); + containerWorkingDirFilePath = new FilePath(containerWorkingDirFilePath.getChannel(), containerWorkingDirFilePathStr); + LOGGER.log(Level.FINEST, "Modified the pwd to match {0} containers workspace directory : {1}", + new String[]{containerName, containerWorkingDirFilePathStr}); + } + } + String[] envVars = starter.envs(); + // modify the working dir on envvars part of starter env vars + if (!containerWorkingDirStr.equals(ContainerTemplate.DEFAULT_WORKING_DIR)) { + for (int i = 0; i < envVars.length; i++) { + String keyValue = envVars[i]; + String[] split = keyValue.split("=", 2); + if (split[1].startsWith(ContainerTemplate.DEFAULT_WORKING_DIR)) { + // Container has a custom workingDir, update env vars with right workspace folder + split[1] = split[1].replaceFirst(ContainerTemplate.DEFAULT_WORKING_DIR, containerWorkingDirStr); + envVars[i] = split[0] + "=" + split[1]; + LOGGER.log(Level.FINEST, "Updated the starter environment variable, key: {0}, Value: {1}", + new String[]{split[0], split[1]}); + } + } + } + if (node != null) { // It seems this is possible despite the method javadoc saying it is non-null final Computer computer = node.toComputer(); if (computer != null) { List resultEnvVar = new ArrayList<>(); try { EnvVars environment = computer.getEnvironment(); - String[] envs = starter.envs(); - for (String keyValue : envs) { - String[] split = keyValue.split("=", 2); - if (!split[1].equals(environment.get(split[0]))) { - // Only keep environment variables that differ from Computer's environment - resultEnvVar.add(keyValue); + if (environment != null) { + Set overriddenKeys = new HashSet<>(); + for (String keyValue : envVars) { + String[] split = keyValue.split("=", 2); + if (!split[1].equals(environment.get(split[0]))) { + // Only keep environment variables that differ from Computer's environment + resultEnvVar.add(keyValue); + overriddenKeys.add(split[0]); + } + } + + // modify the working dir on envvars part of Computer + if (!containerWorkingDirStr.equals(ContainerTemplate.DEFAULT_WORKING_DIR)) { + for (Map.Entry entry : environment.entrySet()) { + if (entry.getValue().startsWith(ContainerTemplate.DEFAULT_WORKING_DIR) + && !overriddenKeys.contains(entry.getKey())) { + // Value should be overridden and is not overridden earlier + String newValue = entry.getValue().replaceFirst(ContainerTemplate.DEFAULT_WORKING_DIR, containerWorkingDirStr); + String keyValue = entry.getKey() + "=" + newValue; + LOGGER.log(Level.FINEST, "Updated the value for envVar, key: {0}, Value: {1}", + new String[]{entry.getKey(), newValue}); + resultEnvVar.add(keyValue); + } + } } + envVars = resultEnvVar.toArray(new String[resultEnvVar.size()]); } - envVars = resultEnvVar.toArray(new String[resultEnvVar.size()]); } catch (InterruptedException e) { throw new IOException("Unable to retrieve environment variables", e); } } } - return doLaunch(starter.quiet(), envVars, starter.stdout(), starter.pwd(), starter.masks(), - getCommands(starter)); + return doLaunch(starter.quiet(), envVars, starter.stdout(), containerWorkingDirFilePath, starter.masks(), + getCommands(starter, containerWorkingDirFilePathStr)); } private Proc doLaunch(boolean quiet, String[] cmdEnvs, OutputStream outputForCaller, FilePath pwd, @@ -531,12 +598,24 @@ private static void doExec(OutputStream stdin, PrintStream out, boolean[] masks, } } - static String[] getCommands(Launcher.ProcStarter starter) { + static String[] getCommands(Launcher.ProcStarter starter, String containerWorkingDirStr) { List allCommands = new ArrayList(); // BourneShellScript.launchWithCookie escapes $ as $$, we convert it to \$ for (String cmd : starter.cmds()) { - allCommands.add(cmd.replaceAll("\\$\\$", "\\\\\\$")); + String fixedCommand = cmd.replaceAll("\\$\\$", "\\\\\\$"); + + String oldRemoteDir = null; + FilePath oldRemoteDirFilepath = starter.pwd(); + if (oldRemoteDirFilepath != null) { + oldRemoteDir = oldRemoteDirFilepath.getRemote(); + } + if (oldRemoteDir != null && ! oldRemoteDir.isEmpty() && + !oldRemoteDir.equals(containerWorkingDirStr) && fixedCommand.contains(oldRemoteDir)) { + // Container has a custom workingDir, update the dir in commands + fixedCommand = fixedCommand.replaceAll(oldRemoteDir, containerWorkingDirStr); + } + allCommands.add(fixedCommand); } return allCommands.toArray(new String[allCommands.size()]); } diff --git a/src/test/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/ContainerExecDecoratorTest.java b/src/test/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/ContainerExecDecoratorTest.java index 9747b7d838..1057a7b788 100644 --- a/src/test/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/ContainerExecDecoratorTest.java +++ b/src/test/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/ContainerExecDecoratorTest.java @@ -36,12 +36,15 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; +import hudson.EnvVars; +import hudson.model.Computer; import org.apache.commons.io.output.TeeOutputStream; import org.apache.commons.lang.RandomStringUtils; import org.csanchez.jenkins.plugins.kubernetes.KubernetesClientProvider; @@ -85,6 +88,7 @@ public class ContainerExecDecoratorTest { private ContainerExecDecorator decorator; private Pod pod; + private KubernetesSlave agent; @Rule public LoggerRule containerExecLogs = new LoggerRule() @@ -108,18 +112,21 @@ public void configureCloud() throws Exception { String image = "busybox"; Container c = new ContainerBuilder().withName(image).withImagePullPolicy("IfNotPresent").withImage(image) .withCommand("cat").withTty(true).build(); + Container d = new ContainerBuilder().withName(image + "1").withImagePullPolicy("IfNotPresent").withImage(image) + .withCommand("cat").withTty(true).withWorkingDir("/home/jenkins/agent1").build(); String podName = "test-command-execution-" + RandomStringUtils.random(5, "bcdfghjklmnpqrstvwxz0123456789"); pod = client.pods().create(new PodBuilder().withNewMetadata().withName(podName) - .withLabels(getLabels(this, name)).endMetadata().withNewSpec().withContainers(c).withNodeSelector(Collections.singletonMap("kubernetes.io/os", "linux")).endSpec().build()); + .withLabels(getLabels(this, name)).endMetadata().withNewSpec().withContainers(c, d).withNodeSelector(Collections.singletonMap("kubernetes.io/os", "linux")).endSpec().build()); System.out.println("Created pod: " + pod.getMetadata().getName()); PodTemplate template = new PodTemplate(); template.setName(pod.getMetadata().getName()); - KubernetesSlave agent = mock(KubernetesSlave.class); + agent = mock(KubernetesSlave.class); when(agent.getNamespace()).thenReturn(client.getNamespace()); when(agent.getPodName()).thenReturn(pod.getMetadata().getName()); - when(agent.getKubernetesCloud()).thenReturn(cloud); + doReturn(cloud).when(agent).getKubernetesCloud(); + when(agent.getPod()).thenReturn(Optional.of(pod)); StepContext context = mock(StepContext.class); when(context.get(Node.class)).thenReturn(agent); @@ -218,7 +225,8 @@ public void testCommandExecutionWithNohup() throws Exception { public void commandsEscaping() { ProcStarter procStarter = new DummyLauncher(null).launch(); procStarter = procStarter.cmds("$$$$", "$$?"); - String[] commands = ContainerExecDecorator.getCommands(procStarter); + + String[] commands = ContainerExecDecorator.getCommands(procStarter, null); assertArrayEquals(new String[] { "\\$\\$", "\\$?" }, commands); } @@ -314,14 +322,53 @@ public void testContainerExecPerformance() throws Exception { } } + @Test + @Issue("JENKINS-58975") + public void testContainerExecOnCustomWorkingDir() throws Exception { + doReturn(null).when((Node)agent).toComputer(); + ProcReturn r = execCommandInContainer("busybox1", agent, false, "env"); + assertTrue("Environment variable workingDir1 should be changed to /home/jenkins/agent1", + r.output.contains("workingDir1=/home/jenkins/agent1")); + assertEquals(0, r.exitCode); + assertFalse(r.proc.isAlive()); + } + + @Test + @Issue("JENKINS-58975") + public void testContainerExecOnCustomWorkingDirWithComputeEnvVars() throws Exception { + EnvVars computeEnvVars = new EnvVars(); + computeEnvVars.put("MyDir", "dir"); + computeEnvVars.put("MyCustomDir", "/home/jenkins/agent"); + Computer computer = mock(Computer.class); + doReturn(computeEnvVars).when(computer).getEnvironment(); + + doReturn(computer).when((Node)agent).toComputer(); + ProcReturn r = execCommandInContainer("busybox1", agent, false, "env"); + assertTrue("Environment variable workingDir1 should be changed to /home/jenkins/agent1", + r.output.contains("workingDir1=/home/jenkins/agent1")); + assertTrue("Environment variable MyCustomDir should be changed to /home/jenkins/agent1", + r.output.contains("MyCustomDir=/home/jenkins/agent1")); + assertEquals(0, r.exitCode); + assertFalse(r.proc.isAlive()); + } + private ProcReturn execCommand(boolean quiet, String... cmd) throws Exception { + return execCommandInContainer(null, null, quiet, cmd); + } + + private ProcReturn execCommandInContainer(String containerName, Node node, boolean quiet, String... cmd) throws Exception { + if (containerName != null && ! containerName.isEmpty()) { + decorator.setContainerName(containerName); + } ByteArrayOutputStream out = new ByteArrayOutputStream(); Launcher launcher = decorator - .decorate(new DummyLauncher(new StreamTaskListener(new TeeOutputStream(out, System.out))), null); + .decorate(new DummyLauncher(new StreamTaskListener(new TeeOutputStream(out, System.out))), node); Map envs = new HashMap<>(100); for (int i = 0; i < 50; i++) { envs.put("aaaaaaaa" + i, "bbbbbbbb"); } + envs.put("workingDir1", "/home/jenkins/agent"); + ContainerExecProc proc = (ContainerExecProc) launcher .launch(launcher.new ProcStarter().pwd("/tmp").cmds(cmd).envs(envs).quiet(quiet)); // wait for proc to finish (shouldn't take long) diff --git a/src/test/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/KubernetesDeclarativeAgentTest.java b/src/test/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/KubernetesDeclarativeAgentTest.java index 6d88c78c3a..dd22736b2b 100644 --- a/src/test/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/KubernetesDeclarativeAgentTest.java +++ b/src/test/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/KubernetesDeclarativeAgentTest.java @@ -130,6 +130,19 @@ public void declarativeCustomWorkspace() throws Exception { r.assertLogContains("Workspace dir is", b); } + @Issue("JENKINS-58975") + @Test + public void declarativeCustomWorkingDir() throws Exception { + assertNotNull(createJobThenScheduleRun()); + r.assertBuildStatusSuccess(r.waitForCompletion(b)); + r.assertLogContains("[jnlp] current dir is /home/jenkins/agent/workspace/declarative Custom Working Dir/foo", b); + r.assertLogContains("[jnlp] WORKSPACE=/home/jenkins/agent/workspace/declarative Custom Working Dir", b); + r.assertLogContains("[maven] current dir is /home/jenkins/wsp1/workspace/declarative Custom Working Dir/foo", b); + r.assertLogContains("[maven] WORKSPACE=/home/jenkins/wsp1/workspace/declarative Custom Working Dir", b); + r.assertLogContains("[default:maven] current dir is /home/jenkins/wsp1/workspace/declarative Custom Working Dir/foo", b); + r.assertLogContains("[default:maven] WORKSPACE=/home/jenkins/wsp1/workspace/declarative Custom Working Dir", b); + } + @Issue("JENKINS-57548") @Test public void declarativeWithNestedExplicitInheritance() throws Exception { diff --git a/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..ca6ee9cea8 --- /dev/null +++ b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/src/test/resources/org/csanchez/jenkins/plugins/kubernetes/pipeline/declarativeCustomWorkingDir.groovy b/src/test/resources/org/csanchez/jenkins/plugins/kubernetes/pipeline/declarativeCustomWorkingDir.groovy new file mode 100644 index 0000000000..7e9290a67b --- /dev/null +++ b/src/test/resources/org/csanchez/jenkins/plugins/kubernetes/pipeline/declarativeCustomWorkingDir.groovy @@ -0,0 +1,48 @@ +pipeline { + agent { + kubernetes { + defaultContainer 'maven' + yaml """ +metadata: + labels: + some-label: some-label-value + class: KubernetesDeclarativeAgentTest +spec: + containers: + - name: jnlp + env: + - name: CONTAINER_ENV_VAR + value: jnlp + - name: maven + image: maven:3.3.9-jdk-8-alpine + workingDir: /home/jenkins/wsp1 + command: + - cat + tty: true + env: + - name: CONTAINER_ENV_VAR + value: maven +""" + } + } + + stages { + stage('Run maven') { + steps { + dir('foo') { + container('jnlp') { + sh 'echo [jnlp] current dir is $(pwd)' + sh 'echo [jnlp] WORKSPACE=$WORKSPACE' + } + container('maven') { + sh 'mvn -version' + sh 'echo [maven] current dir is $(pwd)' + sh 'echo [maven] WORKSPACE=$WORKSPACE' + } + sh 'echo [default:maven] current dir is $(pwd)' + sh 'echo [default:maven] WORKSPACE=$WORKSPACE' + } + } + } + } +} \ No newline at end of file