This is an automated email from the ASF dual-hosted git repository.

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-lang.git


The following commit(s) were added to refs/heads/master by this push:
     new 7e0d6dbd2 Improve container detection by mimicing systemd (#1323)
7e0d6dbd2 is described below

commit 7e0d6dbd2a65d8b36a4c9042565d06af34d489a0
Author: maxxedev <[email protected]>
AuthorDate: Mon Dec 2 05:28:28 2024 -0800

    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
---
 .../apache/commons/lang3/RuntimeEnvironment.java   | 88 +++++++++++-----------
 .../commons/lang3/RuntimeEnvironmentTest.java      | 70 ++++++++++++++---
 2 files changed, 104 insertions(+), 54 deletions(-)

diff --git a/src/main/java/org/apache/commons/lang3/RuntimeEnvironment.java 
b/src/main/java/org/apache/commons/lang3/RuntimeEnvironment.java
index 66b93d86c..3ee8f723c 100644
--- a/src/main/java/org/apache/commons/lang3/RuntimeEnvironment.java
+++ b/src/main/java/org/apache/commons/lang3/RuntimeEnvironment.java
@@ -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.
@@ -30,66 +31,63 @@ import java.util.stream.Stream;
 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));
     }
 
     /**
diff --git a/src/test/java/org/apache/commons/lang3/RuntimeEnvironmentTest.java 
b/src/test/java/org/apache/commons/lang3/RuntimeEnvironmentTest.java
index af0461f75..dc3be2ba7 100644
--- a/src/test/java/org/apache/commons/lang3/RuntimeEnvironmentTest.java
+++ b/src/test/java/org/apache/commons/lang3/RuntimeEnvironmentTest.java
@@ -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());
     }
 }

Reply via email to