Skip to content

Commit

Permalink
Improve container detection by mimicing systemd (#1323)
Browse files Browse the repository at this point in the history
* Improve container detection by checking for special files

* check for FreeBSD file

* mimic SystemD logic to detect container

* Fix RAT report complaint

* Various refactors and improvements

* Fix checkstyle import order
  • Loading branch information
maxxedev authored Dec 2, 2024
1 parent e53ac03 commit 7e0d6db
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 54 deletions.
88 changes: 43 additions & 45 deletions src/main/java/org/apache/commons/lang3/RuntimeEnvironment.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
package org.apache.commons.lang3;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;
import java.util.Arrays;

/**
* Helps query the runtime environment.
Expand All @@ -30,66 +31,63 @@
public class RuntimeEnvironment {

/**
* 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 key The env var key to find.
* @return value The env var value or null
*/
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 key) {
try {
byte[] bytes = Files.readAllBytes(Paths.get(envVarFile));
String content = new String(bytes, Charset.defaultCharset());
// Split by null byte character
String[] lines = content.split("\u0000");
String prefix = key + "=";
return Arrays.stream(lines)
.filter(line -> line.startsWith(prefix))
.map(line -> line.split("=", 2))
.map(keyValue -> keyValue[1])
.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();
return inContainer("");
}

/**
* 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() {
return containsLine("/proc/1/cgroup", "/docker");
}
static boolean inContainer(final String dirPrefix) {
/*
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 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() {
return containsLine("/proc/1/environ", "container=podman");
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.
We check files in the container. According to SystemD:
/.dockerenv is used by Docker.
/run/.containerenv is used by PodMan.
*/
String value = getenv(dirPrefix + "/proc/1/environ", "container");
if (value != null) {
return !value.isEmpty();
}
return fileExists(dirPrefix + "/.dockerenv") || fileExists(dirPrefix + "/run/.containerenv");
}

/**
* 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");
private static boolean fileExists(String path) {
return Files.exists(Paths.get(path));
}

/**
Expand Down
70 changes: 61 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,73 @@

package org.apache.commons.lang3;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;

import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import org.junit.jupiter.api.Test;

/**
* 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);
private static final String simpleEnviron = "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\u0000" +
"HOSTNAME=d62718b69f37\u0000TERM=xterm\u0000HOME=/root\u0000";

private static final String podmanEnviron = "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\u0000" +
"HOSTNAME=d62718b69f37\u0000TERM=xterm\u0000container=podman\u0000HOME=/root\u0000";

private static final String emptyContainer = "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\u0000" +
"HOSTNAME=d62718b69f37\u0000TERM=xterm\u0000container=\u0000HOME=/root\u0000";

@TempDir
private Path tempDir;

private static Arguments[] testIsContainer() {
return new Arguments[]{
Arguments.of("in docker no file", simpleEnviron, null, false),
Arguments.of("in docker with file", simpleEnviron, ".dockerenv", true),
Arguments.of("in podman no file", podmanEnviron, "run/.containerenv", true),
Arguments.of("in podman with file", simpleEnviron, "run/.containerenv", true),
Arguments.of("in podman empty env var no file", emptyContainer, null, false),
Arguments.of("in podman empty env var with file", emptyContainer, "run/.containerenv", false),
Arguments.of("not in container", simpleEnviron, null, false),
Arguments.of("pid1 error no file", null, null, false),
Arguments.of("pid1 error docker file", null, ".dockerenv", true),
Arguments.of("pid1 error podman file", null, ".dockerenv", true),
};
}

@ParameterizedTest
@MethodSource
public void testIsContainer(String label, String environ, String fileToCreate, boolean expected) throws IOException {
assertEquals(expected, doTestInContainer(environ, fileToCreate), label);
}

private boolean doTestInContainer(String environ, String fileToCreate) throws IOException {
Path testDir = tempDir.resolve(UUID.randomUUID().toString());
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 (environ != null) {
Files.write(pid1EnvironFile, environ.getBytes(StandardCharsets.UTF_8));
}

return RuntimeEnvironment.inContainer(testDir.toString());
}
}

0 comments on commit 7e0d6db

Please sign in to comment.