Skip to content

Commit 7e0d6db

Browse files
authored
Improve container detection by mimicing systemd (#1323)
* 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
1 parent e53ac03 commit 7e0d6db

File tree

2 files changed

+104
-54
lines changed

2 files changed

+104
-54
lines changed

src/main/java/org/apache/commons/lang3/RuntimeEnvironment.java

Lines changed: 43 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@
1818
package org.apache.commons.lang3;
1919

2020
import java.io.IOException;
21+
import java.nio.charset.Charset;
2122
import java.nio.file.Files;
2223
import java.nio.file.Paths;
23-
import java.util.stream.Stream;
24+
import java.util.Arrays;
2425

2526
/**
2627
* Helps query the runtime environment.
@@ -30,66 +31,63 @@
3031
public class RuntimeEnvironment {
3132

3233
/**
33-
* Tests whether the file at the given path string contains a specific line.
34+
* Tests whether the /proc/N/environ file at the given path string contains a specific line prefix.
3435
*
35-
* @param path The path to a file.
36-
* @param line The line to find.
37-
* @return whether the file at the given path string contains a specific line.
36+
* @param envVarFile The path to a /proc/N/environ file.
37+
* @param key The env var key to find.
38+
* @return value The env var value or null
3839
*/
39-
private static Boolean containsLine(final String path, final String line) {
40-
try (Stream<String> stream = Files.lines(Paths.get(path))) {
41-
return stream.anyMatch(test -> test.contains(line));
40+
private static String getenv(final String envVarFile, final String key) {
41+
try {
42+
byte[] bytes = Files.readAllBytes(Paths.get(envVarFile));
43+
String content = new String(bytes, Charset.defaultCharset());
44+
// Split by null byte character
45+
String[] lines = content.split("\u0000");
46+
String prefix = key + "=";
47+
return Arrays.stream(lines)
48+
.filter(line -> line.startsWith(prefix))
49+
.map(line -> line.split("=", 2))
50+
.map(keyValue -> keyValue[1])
51+
.findFirst()
52+
.orElse(null);
4253
} catch (final IOException e) {
43-
return false;
54+
return null;
4455
}
4556
}
4657

4758
/**
4859
* Tests whether we are running in a container like Docker or Podman.
4960
*
50-
* @return whether we are running in a container like Docker or Podman.
61+
* @return whether we are running in a container like Docker or Podman. Never null
5162
*/
5263
public static Boolean inContainer() {
53-
return inDocker() || inPodman();
64+
return inContainer("");
5465
}
5566

56-
/**
57-
* Tests whether we are running in a Docker container.
58-
* <p>
59-
* Package-private for testing.
60-
* </p>
61-
*
62-
* @return whether we are running in a Docker container.
63-
*/
64-
// Could be public at a later time.
65-
static Boolean inDocker() {
66-
return containsLine("/proc/1/cgroup", "/docker");
67-
}
67+
static boolean inContainer(final String dirPrefix) {
68+
/*
69+
Roughly follow the logic in SystemD:
70+
https://github.com/systemd/systemd/blob/0747e3b60eb4496ee122066c844210ce818d76d9/src/basic/virt.c#L692
6871
69-
/**
70-
* Tests whether we are running in a Podman container.
71-
* <p>
72-
* Package-private for testing.
73-
* </p>
74-
*
75-
* @return whether we are running in a Podman container.
76-
*/
77-
// Could be public at a later time.
78-
static Boolean inPodman() {
79-
return containsLine("/proc/1/environ", "container=podman");
72+
We check the `container` environment variable of process 1:
73+
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.
74+
If the variable is not empty, we return true.
75+
If the variable is absent, we continue.
76+
77+
We check files in the container. According to SystemD:
78+
/.dockerenv is used by Docker.
79+
/run/.containerenv is used by PodMan.
80+
81+
*/
82+
String value = getenv(dirPrefix + "/proc/1/environ", "container");
83+
if (value != null) {
84+
return !value.isEmpty();
85+
}
86+
return fileExists(dirPrefix + "/.dockerenv") || fileExists(dirPrefix + "/run/.containerenv");
8087
}
8188

82-
/**
83-
* Tests whether we are running in a Windows Subsystem for Linux (WSL).
84-
* <p>
85-
* Package-private for testing.
86-
* </p>
87-
*
88-
* @return whether we are running in a Windows Subsystem for Linux (WSL).
89-
*/
90-
// Could be public at a later time.
91-
static Boolean inWsl() {
92-
return containsLine("/proc/1/environ", "container=wslcontainer_host_id");
89+
private static boolean fileExists(String path) {
90+
return Files.exists(Paths.get(path));
9391
}
9492

9593
/**

src/test/java/org/apache/commons/lang3/RuntimeEnvironmentTest.java

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,73 @@
1717

1818
package org.apache.commons.lang3;
1919

20-
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
20+
import static org.junit.jupiter.api.Assertions.assertEquals;
21+
22+
import java.io.IOException;
23+
import java.nio.charset.StandardCharsets;
24+
import java.nio.file.Files;
25+
import java.nio.file.Path;
26+
import java.util.UUID;
27+
28+
import org.junit.jupiter.api.io.TempDir;
29+
import org.junit.jupiter.params.ParameterizedTest;
30+
import org.junit.jupiter.params.provider.Arguments;
31+
import org.junit.jupiter.params.provider.MethodSource;
2132

22-
import org.junit.jupiter.api.Test;
2333

2434
/**
2535
* Tests {@link RuntimeEnvironment}.
2636
*/
2737
public class RuntimeEnvironmentTest {
2838

29-
@Test
30-
public void testIsContainer() {
31-
// At least make sure it does not blow up.
32-
assertDoesNotThrow(RuntimeEnvironment::inContainer);
33-
assertDoesNotThrow(RuntimeEnvironment::inDocker);
34-
assertDoesNotThrow(RuntimeEnvironment::inPodman);
35-
assertDoesNotThrow(RuntimeEnvironment::inWsl);
39+
private static final String simpleEnviron = "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\u0000" +
40+
"HOSTNAME=d62718b69f37\u0000TERM=xterm\u0000HOME=/root\u0000";
41+
42+
private static final String podmanEnviron = "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\u0000" +
43+
"HOSTNAME=d62718b69f37\u0000TERM=xterm\u0000container=podman\u0000HOME=/root\u0000";
44+
45+
private static final String emptyContainer = "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\u0000" +
46+
"HOSTNAME=d62718b69f37\u0000TERM=xterm\u0000container=\u0000HOME=/root\u0000";
47+
48+
@TempDir
49+
private Path tempDir;
50+
51+
private static Arguments[] testIsContainer() {
52+
return new Arguments[]{
53+
Arguments.of("in docker no file", simpleEnviron, null, false),
54+
Arguments.of("in docker with file", simpleEnviron, ".dockerenv", true),
55+
Arguments.of("in podman no file", podmanEnviron, "run/.containerenv", true),
56+
Arguments.of("in podman with file", simpleEnviron, "run/.containerenv", true),
57+
Arguments.of("in podman empty env var no file", emptyContainer, null, false),
58+
Arguments.of("in podman empty env var with file", emptyContainer, "run/.containerenv", false),
59+
Arguments.of("not in container", simpleEnviron, null, false),
60+
Arguments.of("pid1 error no file", null, null, false),
61+
Arguments.of("pid1 error docker file", null, ".dockerenv", true),
62+
Arguments.of("pid1 error podman file", null, ".dockerenv", true),
63+
};
64+
}
65+
66+
@ParameterizedTest
67+
@MethodSource
68+
public void testIsContainer(String label, String environ, String fileToCreate, boolean expected) throws IOException {
69+
assertEquals(expected, doTestInContainer(environ, fileToCreate), label);
70+
}
71+
72+
private boolean doTestInContainer(String environ, String fileToCreate) throws IOException {
73+
Path testDir = tempDir.resolve(UUID.randomUUID().toString());
74+
Path pid1EnvironFile = testDir.resolve("proc/1/environ");
75+
Files.createDirectories(pid1EnvironFile.getParent());
76+
77+
if (fileToCreate != null) {
78+
Path file = testDir.resolve(fileToCreate);
79+
Files.createDirectories(file.getParent());
80+
Files.createFile(file);
81+
}
82+
83+
if (environ != null) {
84+
Files.write(pid1EnvironFile, environ.getBytes(StandardCharsets.UTF_8));
85+
}
86+
87+
return RuntimeEnvironment.inContainer(testDir.toString());
3688
}
3789
}

0 commit comments

Comments
 (0)