Repository: pre-commit/pre-commit

Issue:
Failure to get the container id
With #1888 in place, there's a regression when running inside a container. There's an assumption in https://github.com/pre-commit/pre-commit/blob/master/pre_commit/languages/docker.py#L32 that the hostname is the container ID which is not always the case (it's easy enough to set a different hostname with `docker run --hostname foo`). It causes the `docker inspect` command that follows to fail. A more reliable way to get the container id is from `/proc/1/cpuset` or from the first line in `/proc/1/cgroup` which is already checked in `_is_in_docker`.

Thanks @asottile and @okainov for your work on #1387 and pre-commit in general.


Discussion:

Resolving patch:
diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py
--- a/pre_commit/languages/docker.py
+++ b/pre_commit/languages/docker.py
@@ -1,7 +1,6 @@
 import hashlib
 import json
 import os
-import socket
 from typing import Sequence
 from typing import Tuple
 
@@ -26,12 +25,24 @@ def _is_in_docker() -> bool:
         return False
 
 
+def _get_container_id() -> str:
+    # It's assumed that we already check /proc/1/cgroup in _is_in_docker. The
+    # cpuset cgroup controller existed since cgroups were introduced so this
+    # way of getting the container ID is pretty reliable.
+    with open('/proc/1/cgroup', 'rb') as f:
+        for line in f.readlines():
+            if line.split(b':')[1] == b'cpuset':
+                return os.path.basename(line.split(b':')[2]).strip().decode()
+    raise RuntimeError('Failed to find the container ID in /proc/1/cgroup.')
+
+
 def _get_docker_path(path: str) -> str:
     if not _is_in_docker():
         return path
-    hostname = socket.gethostname()
 
-    _, out, _ = cmd_output_b('docker', 'inspect', hostname)
+    container_id = _get_container_id()
+
+    _, out, _ = cmd_output_b('docker', 'inspect', container_id)
 
     container, = json.loads(out)
     for mount in container['Mounts']:


Test patch:
diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py
--- a/tests/languages/docker_test.py
+++ b/tests/languages/docker_test.py
@@ -9,6 +9,41 @@
 
 from pre_commit.languages import docker
 
+DOCKER_CGROUP_EXAMPLE = b'''\
+12:hugetlb:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
+11:blkio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
+10:freezer:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
+9:cpu,cpuacct:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
+8:pids:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
+7:rdma:/
+6:net_cls,net_prio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
+5:cpuset:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
+4:devices:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
+3:memory:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
+2:perf_event:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
+1:name=systemd:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
+0::/system.slice/containerd.service
+'''  # noqa: E501
+
+# The ID should match the above cgroup example.
+CONTAINER_ID = 'c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7'  # noqa: E501
+
+NON_DOCKER_CGROUP_EXAMPLE = b'''\
+12:perf_event:/
+11:hugetlb:/
+10:devices:/
+9:blkio:/
+8:rdma:/
+7:cpuset:/
+6:cpu,cpuacct:/
+5:freezer:/
+4:memory:/
+3:pids:/
+2:net_cls,net_prio:/
+1:name=systemd:/init.scope
+0::/init.scope
+'''
+
 
 def test_docker_fallback_user():
     def invalid_attribute():
@@ -37,45 +72,25 @@ def _mock_open(data):
 
 
 def test_in_docker_docker_in_file():
-    docker_cgroup_example = b'''\
-12:hugetlb:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
-11:blkio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
-10:freezer:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
-9:cpu,cpuacct:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
-8:pids:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
-7:rdma:/
-6:net_cls,net_prio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
-5:cpuset:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
-4:devices:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
-3:memory:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
-2:perf_event:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
-1:name=systemd:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
-0::/system.slice/containerd.service
-'''  # noqa: E501
-    with _mock_open(docker_cgroup_example):
+    with _mock_open(DOCKER_CGROUP_EXAMPLE):
         assert docker._is_in_docker() is True
 
 
 def test_in_docker_docker_not_in_file():
-    non_docker_cgroup_example = b'''\
-12:perf_event:/
-11:hugetlb:/
-10:devices:/
-9:blkio:/
-8:rdma:/
-7:cpuset:/
-6:cpu,cpuacct:/
-5:freezer:/
-4:memory:/
-3:pids:/
-2:net_cls,net_prio:/
-1:name=systemd:/init.scope
-0::/init.scope
-'''
-    with _mock_open(non_docker_cgroup_example):
+    with _mock_open(NON_DOCKER_CGROUP_EXAMPLE):
         assert docker._is_in_docker() is False
 
 
+def test_get_container_id():
+    with _mock_open(DOCKER_CGROUP_EXAMPLE):
+        assert docker._get_container_id() == CONTAINER_ID
+
+
+def test_get_container_id_failure():
+    with _mock_open(b''), pytest.raises(RuntimeError):
+        docker._get_container_id()
+
+
 def test_get_docker_path_not_in_docker_returns_same():
     with mock.patch.object(docker, '_is_in_docker', return_value=False):
         assert docker._get_docker_path('abc') == 'abc'
@@ -84,7 +99,10 @@ def test_get_docker_path_not_in_docker_returns_same():
 @pytest.fixture
 def in_docker():
     with mock.patch.object(docker, '_is_in_docker', return_value=True):
-        yield
+        with mock.patch.object(
+            docker, '_get_container_id', return_value=CONTAINER_ID,
+        ):
+            yield
 
 
 def _linux_commonpath():