From 17c5a69477e6346234c4ba40a4e70d47a3b264c0 Mon Sep 17 00:00:00 2001 From: Kyle Cronin Date: Tue, 20 May 2025 23:11:19 -0400 Subject: [PATCH] Add PodContainerSource Extension Point Add PodContainerSource extension point to lookup container working directory. This allows plugins to use ContainerExecDecorator with alternate container sources, like ephemeral containers. --- .../kubernetes/PodContainerSource.java | 68 +++++++++++++ .../pipeline/ContainerExecDecorator.java | 23 ++--- .../kubernetes/PodContainerSourceTest.java | 96 +++++++++++++++++++ 3 files changed, 172 insertions(+), 15 deletions(-) create mode 100644 src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodContainerSource.java create mode 100644 src/test/java/org/csanchez/jenkins/plugins/kubernetes/PodContainerSourceTest.java diff --git a/src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodContainerSource.java b/src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodContainerSource.java new file mode 100644 index 0000000000..cf921f7bfa --- /dev/null +++ b/src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodContainerSource.java @@ -0,0 +1,68 @@ +package org.csanchez.jenkins.plugins.kubernetes; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.ExtensionList; +import hudson.ExtensionPoint; +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.PodSpec; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * Pod container sources are responsible to locating details about Pod containers. + */ +public abstract class PodContainerSource implements ExtensionPoint { + + /** + * Lookup the working directory of the named container. + * @param pod pod reference to lookup container in + * @param containerName name of container to lookup + * @return working directory path if container found and working dir specified, otherwise empty + */ + public abstract Optional getContainerWorkingDir(@NonNull Pod pod, @NonNull String containerName); + + /** + * Lookup all {@link PodContainerSource} extensions. + * @return pod container source extension list + */ + @NonNull + public static List all() { + return ExtensionList.lookup(PodContainerSource.class); + } + + /** + * Lookup pod container working dir. Searches all {@link PodContainerSource} extensions and returns + * the first non-empty result. + * @param pod pod to inspect + * @param containerName container to search for + * @return optional working dir if container found and working dir, possibly empty + */ + public static Optional lookupContainerWorkingDir(@NonNull Pod pod, @NonNull String containerName) { + return all().stream() + .map(cs -> cs.getContainerWorkingDir(pod, containerName)) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst(); + } + + /** + * Default implementation of {@link PodContainerSource} that only searches the primary + * pod containers. Ephemeral or init containers are not included container lookups in + * this implementation. + * @see PodSpec#getContainers() + */ + @Extension + public static final class DefaultPodContainerSource extends PodContainerSource { + + @Override + public Optional getContainerWorkingDir(@NonNull Pod pod, @NonNull String containerName) { + return pod.getSpec().getContainers().stream() + .filter(c -> Objects.equals(c.getName(), containerName)) + .findAny() + .map(Container::getWorkingDir); + } + } +} 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 dc578e93be..bb30f65e30 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 @@ -27,7 +27,6 @@ import hudson.Proc; import hudson.model.Computer; import hudson.model.Node; -import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.dsl.ExecListener; @@ -59,6 +58,7 @@ import org.apache.commons.io.output.TeeOutputStream; import org.csanchez.jenkins.plugins.kubernetes.ContainerTemplate; import org.csanchez.jenkins.plugins.kubernetes.KubernetesSlave; +import org.csanchez.jenkins.plugins.kubernetes.PodContainerSource; import org.jenkinsci.plugins.workflow.steps.EnvironmentExpander; /** @@ -284,13 +284,8 @@ public Proc launch(ProcStarter starter) throws IOException { : 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()); - } + Optional containerWorkingDir = PodContainerSource.lookupContainerWorkingDir( + slave.getPod().get(), containerName); if (containerWorkingDir.isPresent()) { containerWorkingDirStr = containerWorkingDir.get(); } @@ -403,7 +398,7 @@ private Proc doLaunch( // Do not send this command to the output when in quiet mode if (quiet) { stream = toggleStdout; - printStream = new PrintStream(stream, true, StandardCharsets.UTF_8.toString()); + printStream = new PrintStream(stream, true, StandardCharsets.UTF_8); } else { printStream = launcher.getListener().getLogger(); stream = new TeeOutputStream(toggleStdout, printStream); @@ -575,7 +570,7 @@ public void onClose(int i, String s) { } toggleStdout.disable(); OutputStream stdin = watch.getInput(); - PrintStream in = new PrintStream(stdin, true, StandardCharsets.UTF_8.name()); + PrintStream in = new PrintStream(stdin, true, StandardCharsets.UTF_8); if (!launcher.isUnix()) { in.print("@echo off"); in.print(newLine(true)); @@ -583,7 +578,7 @@ public void onClose(int i, String s) { if (pwd != null) { // We need to get into the project workspace. // The workspace is not known in advance, so we have to execute a cd command. - in.print(String.format("cd \"%s\"", pwd)); + in.printf("cd \"%s\"", pwd); in.print(newLine(!launcher.isUnix())); } @@ -644,10 +639,8 @@ public void onClose(int i, String s) { } doExec(in, !launcher.isUnix(), printStream, masks, commands); - LOGGER.log( - Level.INFO, - "Created process inside pod: [" + getPodName() + "], container: [" + containerName + "]" - + "[" + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startMethod) + " ms]"); + LOGGER.fine(() -> "Created process inside pod: [" + getPodName() + "], container: [" + containerName + + "]" + "[" + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startMethod) + " ms]"); ContainerExecProc proc = new ContainerExecProc(watch, alive, finished, stdin, printStream); closables.add(proc); return proc; diff --git a/src/test/java/org/csanchez/jenkins/plugins/kubernetes/PodContainerSourceTest.java b/src/test/java/org/csanchez/jenkins/plugins/kubernetes/PodContainerSourceTest.java new file mode 100644 index 0000000000..7e18138645 --- /dev/null +++ b/src/test/java/org/csanchez/jenkins/plugins/kubernetes/PodContainerSourceTest.java @@ -0,0 +1,96 @@ +package org.csanchez.jenkins.plugins.kubernetes; + +import static org.junit.Assert.*; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import io.fabric8.kubernetes.api.model.EphemeralContainer; +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.PodBuilder; +import java.util.Optional; +import org.apache.commons.lang3.StringUtils; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.WithoutJenkins; + +public class PodContainerSourceTest { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Test + public void lookupContainerWorkingDir() { + Pod pod = new PodBuilder() + .withNewSpec() + .addNewContainer() + .withName("foo") + .withWorkingDir("/app/foo") + .endContainer() + .addNewEphemeralContainer() + .withName("bar") + .withWorkingDir("/app/bar") + .endEphemeralContainer() + .addNewEphemeralContainer() + .withName("foo") + .withWorkingDir("/app/ephemeral-foo") + .endEphemeralContainer() + .endSpec() + .build(); + + Optional wd = PodContainerSource.lookupContainerWorkingDir(pod, "foo"); + assertTrue(wd.isPresent()); + assertEquals("/app/foo", wd.get()); + + // should use TestPodContainerSource to find ephemeral container + wd = PodContainerSource.lookupContainerWorkingDir(pod, "bar"); + assertTrue(wd.isPresent()); + assertEquals("/app/bar", wd.get()); + + // no named container + wd = PodContainerSource.lookupContainerWorkingDir(pod, "fish"); + assertFalse(wd.isPresent()); + } + + @WithoutJenkins + @Test + public void defaultPodContainerSourceGetContainerWorkingDir() { + Pod pod = new PodBuilder() + .withNewSpec() + .addNewContainer() + .withName("foo") + .withWorkingDir("/app/foo") + .endContainer() + .addNewEphemeralContainer() + .withName("bar") + .withWorkingDir("/app/bar") + .endEphemeralContainer() + .endSpec() + .build(); + + PodContainerSource.DefaultPodContainerSource source = new PodContainerSource.DefaultPodContainerSource(); + Optional wd = source.getContainerWorkingDir(pod, "foo"); + assertTrue(wd.isPresent()); + assertEquals("/app/foo", wd.get()); + + // should not return ephemeral container + wd = source.getContainerWorkingDir(pod, "bar"); + assertFalse(wd.isPresent()); + + // no named container + wd = source.getContainerWorkingDir(pod, "fish"); + assertFalse(wd.isPresent()); + } + + @Extension + public static class TestPodContainerSource extends PodContainerSource { + + @Override + public Optional getContainerWorkingDir(@NonNull Pod pod, @NonNull String containerName) { + return pod.getSpec().getEphemeralContainers().stream() + .filter(c -> StringUtils.equals(c.getName(), containerName)) + .findAny() + .map(EphemeralContainer::getWorkingDir); + } + } +}