Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<String> getContainerWorkingDir(@NonNull Pod pod, @NonNull String containerName);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering whether we have any guarantee that a given name can't be used by both containers and ephemeral containers, as it would create an ambiguity...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, would public abstract Optional<Container> getContainerWorkingDir(Pod, String) be more flexible?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering whether we have any guarantee that a given name can't be used by both containers and ephemeral containers, as it would create an ambiguity...

https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#Container

Name of the container specified as a DNS_LABEL. Each container in a pod must have a unique name (DNS_LABEL). Cannot be updated.

This is confirmed if you attempt to apply a spec with duplicate names. It doesn't matter if they are defined in containers, initContainers, or ephemeralContainers.

❯ kubectl apply -f deployment-with-dup-container-names.yaml
The Deployment "foo" is invalid: spec.template.spec.initContainers[0].name: Duplicate value: "a"

Also, would public abstract Optional<Container> getContainerWorkingDir(Pod, String) be more flexible?

Yes, I will refactor to return the Container instance.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, would public abstract Optional<Container> getContainerWorkingDir(Pod, String) be more flexible?

I take back what I said 😄 . While it makes sense at first, fabric8 models Container and EphemeralContainer as different types. From the previous PR it sounded like the whole purpose of this extension is to isolate ephemeral container references to extension plugins, so this extension is to abstract over those differences.

In a future PR I will be adding 1 more method to PodContainerSource to get the container status. Statuses are not properties of Container or EphemeralContainer, they are accesses via pod.getStatus().getContainerStatuses().

public abstract Optional<ContainerStatus> getContainerStatus(@NonNull Pod pod, @NonNull String containerName);

These 2 methods will allow my extension to reuse ContainerExecDecorator, as shown in this PR, and display ephemeral container logs (future PR that updates KubernetesComputer#goContainerLog).

ContainerExecDecorator does A LOT of complicated stuff which would be a shame to replicate and maintain separately given this small abstraction makes it reusable. Hopefully we can figure out an acceptable solution.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fabric8 models Container and EphemeralContainer as different types

😞

Indeed, I don't see anything better.


/**
* Lookup all {@link PodContainerSource} extensions.
* @return pod container source extension list
*/
@NonNull
public static List<PodContainerSource> 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<String> 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<String> getContainerWorkingDir(@NonNull Pod pod, @NonNull String containerName) {
return pod.getSpec().getContainers().stream()
.filter(c -> Objects.equals(c.getName(), containerName))
.findAny()
.map(Container::getWorkingDir);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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> container = slave.getPod().get().getSpec().getContainers().stream()
.filter(container1 -> container1.getName().equals(containerName))
.findAny();
Optional<String> containerWorkingDir = Optional.empty();
if (container.isPresent() && container.get().getWorkingDir() != null) {
containerWorkingDir = Optional.of(container.get().getWorkingDir());
}
Optional<String> containerWorkingDir = PodContainerSource.lookupContainerWorkingDir(
slave.getPod().get(), containerName);
if (containerWorkingDir.isPresent()) {
containerWorkingDirStr = containerWorkingDir.get();
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -575,15 +570,15 @@ 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));
}
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()));
}

Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> 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<String> getContainerWorkingDir(@NonNull Pod pod, @NonNull String containerName) {
return pod.getSpec().getEphemeralContainers().stream()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a real pod, pod.getSpec().getEphemeralContainers() could be null (just mentioning for your production extension)

.filter(c -> StringUtils.equals(c.getName(), containerName))
.findAny()
.map(EphemeralContainer::getWorkingDir);
}
}
}