Skip to content

Commit

Permalink
mimic SystemD logic to detect container
Browse files Browse the repository at this point in the history
  • Loading branch information
maxxedev committed Dec 1, 2024
1 parent 90b76f4 commit 6bac146
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 63 deletions.
92 changes: 38 additions & 54 deletions src/main/java/org/apache/commons/lang3/RuntimeEnvironment.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;
import java.util.Arrays;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
* Helps query the runtime environment.
Expand All @@ -29,79 +31,61 @@
*/
public class RuntimeEnvironment {

// package-private non-static fields for testing.
static String rootDir = "/";

/**
* Tests whether the file at the given path string contains a specific line.
* Tests whether the /proc/N/environ file at the given path string contains a specific line prefix.
*
* @param path The path to a file.
* @param line The line to find.
* @return whether the file at the given path string contains a specific line.
* @param envVarFile The path to a /proc/N/environ file.
* @param prefix The line prefix to find.
* @return value after the prefix
*/
private static Boolean containsLine(final String path, final String line) {
try (Stream<String> stream = Files.lines(Paths.get(path))) {
return stream.anyMatch(test -> test.contains(line));
private static String getenv(final String envVarFile, final String prefix) {
try {
byte[] bytes = Files.readAllBytes(Paths.get(envVarFile));
String content = new String(bytes, UTF_8);
// Split by null byte character
String[] lines = content.split("\u0000");
return Arrays.stream(lines).filter(test -> test.startsWith(prefix))
.map(test -> StringUtils.substringAfter(test, prefix))
.findFirst()
.orElse(null);
} catch (final IOException e) {
return false;
return null;
}
}

/**
* Tests whether we are running in a container like Docker or Podman.
*
* @return whether we are running in a container like Docker or Podman.
* @return whether we are running in a container like Docker or Podman. Never null
*/
public static Boolean inContainer() {
return inDocker() || inPodman();
}
/*
Roughly follow the logic in SystemD:
https://github.com/systemd/systemd/blob/0747e3b60eb4496ee122066c844210ce818d76d9/src/basic/virt.c#L692
/**
* Tests whether we are running in a Docker container.
* <p>
* Package-private for testing.
* </p>
*
* @return whether we are running in a Docker container.
*/
// Could be public at a later time.
static Boolean inDocker() {
if (fileExists("/.dockerenv")) {
return true;
}
return containsLine("/proc/1/cgroup", "/docker");
}
We check the `container` environment variable of process 1:
If the variable is empty, we return false. This includes the case, where the container developer wants to hide the fact that the application runs in a container.
If the variable is not empty, we return true.
If the variable is absent, we continue.
/**
* Tests whether we are running in a Podman container.
* <p>
* Package-private for testing.
* </p>
*
* @return whether we are running in a Podman container.
*/
// Could be public at a later time.
static Boolean inPodman() {
if (fileExists("/run/.containerenv") || fileExists("/var/run/.containerenv")) {
return true;
}
return containsLine("/proc/1/environ", "container=podman");
We check files in the container. According to SystemD:
/.dockerenv is used by Docker.
/run/.containerenv is used by PodMan.
*/
String value = getenv(rootDir + "proc/1/environ", "container=");
return StringUtils.isNotEmpty(value)
|| fileExists(rootDir + ".dockerenv")
|| fileExists(rootDir + "run/.containerenv");
}

private static boolean fileExists(String path) {
return Files.exists(Paths.get(path));
}

/**
* Tests whether we are running in a Windows Subsystem for Linux (WSL).
* <p>
* Package-private for testing.
* </p>
*
* @return whether we are running in a Windows Subsystem for Linux (WSL).
*/
// Could be public at a later time.
static Boolean inWsl() {
return containsLine("/proc/1/environ", "container=wslcontainer_host_id");
}

/**
* Constructs a new instance.
*
Expand Down
80 changes: 71 additions & 9 deletions src/test/java/org/apache/commons/lang3/RuntimeEnvironmentTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,83 @@

package org.apache.commons.lang3;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.io.TempDir;

import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;

/**
* Tests {@link RuntimeEnvironment}.
*/
public class RuntimeEnvironmentTest {

@Test
public void testIsContainer() {
// At least make sure it does not blow up.
assertDoesNotThrow(RuntimeEnvironment::inContainer);
assertDoesNotThrow(RuntimeEnvironment::inDocker);
assertDoesNotThrow(RuntimeEnvironment::inPodman);
assertDoesNotThrow(RuntimeEnvironment::inWsl);
@TempDir
private Path tempDir;

@AfterAll
public static void tearDown() {
RuntimeEnvironment.rootDir = "/";
}

@TestFactory
public DynamicTest[] testIsContainerDocker() {
return new DynamicTest[]{
dynamicTest("in docker no file", () ->
assertFalse(doTestInContainer("RuntimeEnvironmentTest.docker.txt", null))),

dynamicTest("in docker with file", () ->
assertTrue(doTestInContainer("RuntimeEnvironmentTest.docker.txt", ".dockerenv"))),

dynamicTest("in podman no file", () ->
assertTrue(doTestInContainer("RuntimeEnvironmentTest.podman.txt", null))),

dynamicTest("in podman with file", () ->
assertTrue(doTestInContainer("RuntimeEnvironmentTest.none.txt", "run/.containerenv"))),

dynamicTest("not in container", () ->
assertFalse(doTestInContainer("RuntimeEnvironmentTest.none.txt", null))),

dynamicTest("pid1 error no file", () ->
assertFalse(doTestInContainer(null, null))),

dynamicTest("pid1 error docker file", () ->
assertTrue(doTestInContainer(null, ".dockerenv"))),

dynamicTest("pid1 error podman file", () ->
assertTrue(doTestInContainer(null, ".dockerenv"))),
};
}

private boolean doTestInContainer(String envFile, String fileToCreate) throws IOException {
Path testDir = tempDir.resolve(UUID.randomUUID().toString());
RuntimeEnvironment.rootDir = testDir + "/";
Path pid1EnvironFile = testDir.resolve("proc/1/environ");
Files.createDirectories(pid1EnvironFile.getParent());

if (fileToCreate != null) {
Path file = testDir.resolve(fileToCreate);
Files.createDirectories(file.getParent());
Files.createFile(file);
}

if (envFile != null) {
URL resource = RuntimeEnvironmentTest.class.getResource(envFile);
try (InputStream in = resource.openStream()) {
Files.copy(in, pid1EnvironFile);
}
}

return RuntimeEnvironment.inContainer();
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 comments on commit 6bac146

Please sign in to comment.