Skip to content

Commit 7f13fa5

Browse files
authored
Merge pull request pre-commit#1888 from okainov/docker-fix-2
fix: fix path mounting when running in Docker
2 parents 52e1dd6 + 6d5d386 commit 7f13fa5

File tree

2 files changed

+176
-4
lines changed

2 files changed

+176
-4
lines changed

pre_commit/languages/docker.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import hashlib
2+
import json
23
import os
4+
import socket
35
from typing import Sequence
46
from typing import Tuple
57

@@ -8,13 +10,42 @@
810
from pre_commit.languages import helpers
911
from pre_commit.prefix import Prefix
1012
from pre_commit.util import clean_path_on_failure
13+
from pre_commit.util import cmd_output_b
1114

1215
ENVIRONMENT_DIR = 'docker'
1316
PRE_COMMIT_LABEL = 'PRE_COMMIT'
1417
get_default_version = helpers.basic_get_default_version
1518
healthy = helpers.basic_healthy
1619

1720

21+
def _is_in_docker() -> bool:
22+
try:
23+
with open('/proc/1/cgroup', 'rb') as f:
24+
return b'docker' in f.read()
25+
except FileNotFoundError:
26+
return False
27+
28+
29+
def _get_docker_path(path: str) -> str:
30+
if not _is_in_docker():
31+
return path
32+
hostname = socket.gethostname()
33+
34+
_, out, _ = cmd_output_b('docker', 'inspect', hostname)
35+
36+
container, = json.loads(out)
37+
for mount in container['Mounts']:
38+
src_path = mount['Source']
39+
to_path = mount['Destination']
40+
if os.path.commonpath((path, to_path)) == to_path:
41+
# So there is something in common,
42+
# and we can proceed remapping it
43+
return path.replace(to_path, src_path)
44+
# we're in Docker, but the path is not mounted, cannot really do anything,
45+
# so fall back to original path
46+
return path
47+
48+
1849
def md5(s: str) -> str: # pragma: win32 no cover
1950
return hashlib.md5(s.encode()).hexdigest()
2051

@@ -73,7 +104,7 @@ def docker_cmd() -> Tuple[str, ...]: # pragma: win32 no cover
73104
# https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from
74105
# The `Z` option tells Docker to label the content with a private
75106
# unshared label. Only the current container can use a private volume.
76-
'-v', f'{os.getcwd()}:/src:rw,Z',
107+
'-v', f'{_get_docker_path(os.getcwd())}:/src:rw,Z',
77108
'--workdir', '/src',
78109
)
79110

tests/languages/docker_test.py

Lines changed: 144 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,155 @@
1+
import builtins
2+
import json
3+
import ntpath
4+
import os.path
5+
import posixpath
16
from unittest import mock
27

8+
import pytest
9+
310
from pre_commit.languages import docker
411

512

613
def test_docker_fallback_user():
714
def invalid_attribute():
815
raise AttributeError
16+
917
with mock.patch.multiple(
10-
'os', create=True,
11-
getuid=invalid_attribute,
12-
getgid=invalid_attribute,
18+
'os', create=True,
19+
getuid=invalid_attribute,
20+
getgid=invalid_attribute,
1321
):
1422
assert docker.get_docker_user() == ()
23+
24+
25+
def test_in_docker_no_file():
26+
with mock.patch.object(builtins, 'open', side_effect=FileNotFoundError):
27+
assert docker._is_in_docker() is False
28+
29+
30+
def _mock_open(data):
31+
return mock.patch.object(
32+
builtins,
33+
'open',
34+
new_callable=mock.mock_open,
35+
read_data=data,
36+
)
37+
38+
39+
def test_in_docker_docker_in_file():
40+
docker_cgroup_example = b'''\
41+
12:hugetlb:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
42+
11:blkio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
43+
10:freezer:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
44+
9:cpu,cpuacct:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
45+
8:pids:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
46+
7:rdma:/
47+
6:net_cls,net_prio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
48+
5:cpuset:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
49+
4:devices:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
50+
3:memory:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
51+
2:perf_event:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
52+
1:name=systemd:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
53+
0::/system.slice/containerd.service
54+
''' # noqa: E501
55+
with _mock_open(docker_cgroup_example):
56+
assert docker._is_in_docker() is True
57+
58+
59+
def test_in_docker_docker_not_in_file():
60+
non_docker_cgroup_example = b'''\
61+
12:perf_event:/
62+
11:hugetlb:/
63+
10:devices:/
64+
9:blkio:/
65+
8:rdma:/
66+
7:cpuset:/
67+
6:cpu,cpuacct:/
68+
5:freezer:/
69+
4:memory:/
70+
3:pids:/
71+
2:net_cls,net_prio:/
72+
1:name=systemd:/init.scope
73+
0::/init.scope
74+
'''
75+
with _mock_open(non_docker_cgroup_example):
76+
assert docker._is_in_docker() is False
77+
78+
79+
def test_get_docker_path_not_in_docker_returns_same():
80+
with mock.patch.object(docker, '_is_in_docker', return_value=False):
81+
assert docker._get_docker_path('abc') == 'abc'
82+
83+
84+
@pytest.fixture
85+
def in_docker():
86+
with mock.patch.object(docker, '_is_in_docker', return_value=True):
87+
yield
88+
89+
90+
def _linux_commonpath():
91+
return mock.patch.object(os.path, 'commonpath', posixpath.commonpath)
92+
93+
94+
def _nt_commonpath():
95+
return mock.patch.object(os.path, 'commonpath', ntpath.commonpath)
96+
97+
98+
def _docker_output(out):
99+
ret = (0, out, b'')
100+
return mock.patch.object(docker, 'cmd_output_b', return_value=ret)
101+
102+
103+
def test_get_docker_path_in_docker_no_binds_same_path(in_docker):
104+
docker_out = json.dumps([{'Mounts': []}]).encode()
105+
106+
with _docker_output(docker_out):
107+
assert docker._get_docker_path('abc') == 'abc'
108+
109+
110+
def test_get_docker_path_in_docker_binds_path_equal(in_docker):
111+
binds_list = [{'Source': '/opt/my_code', 'Destination': '/project'}]
112+
docker_out = json.dumps([{'Mounts': binds_list}]).encode()
113+
114+
with _linux_commonpath(), _docker_output(docker_out):
115+
assert docker._get_docker_path('/project') == '/opt/my_code'
116+
117+
118+
def test_get_docker_path_in_docker_binds_path_complex(in_docker):
119+
binds_list = [{'Source': '/opt/my_code', 'Destination': '/project'}]
120+
docker_out = json.dumps([{'Mounts': binds_list}]).encode()
121+
122+
with _linux_commonpath(), _docker_output(docker_out):
123+
path = '/project/test/something'
124+
assert docker._get_docker_path(path) == '/opt/my_code/test/something'
125+
126+
127+
def test_get_docker_path_in_docker_no_substring(in_docker):
128+
binds_list = [{'Source': '/opt/my_code', 'Destination': '/project'}]
129+
docker_out = json.dumps([{'Mounts': binds_list}]).encode()
130+
131+
with _linux_commonpath(), _docker_output(docker_out):
132+
path = '/projectSuffix/test/something'
133+
assert docker._get_docker_path(path) == path
134+
135+
136+
def test_get_docker_path_in_docker_binds_path_many_binds(in_docker):
137+
binds_list = [
138+
{'Source': '/something_random', 'Destination': '/not-related'},
139+
{'Source': '/opt/my_code', 'Destination': '/project'},
140+
{'Source': '/something-random-2', 'Destination': '/not-related-2'},
141+
]
142+
docker_out = json.dumps([{'Mounts': binds_list}]).encode()
143+
144+
with _linux_commonpath(), _docker_output(docker_out):
145+
assert docker._get_docker_path('/project') == '/opt/my_code'
146+
147+
148+
def test_get_docker_path_in_docker_windows(in_docker):
149+
binds_list = [{'Source': r'c:\users\user', 'Destination': r'c:\folder'}]
150+
docker_out = json.dumps([{'Mounts': binds_list}]).encode()
151+
152+
with _nt_commonpath(), _docker_output(docker_out):
153+
path = r'c:\folder\test\something'
154+
expected = r'c:\users\user\test\something'
155+
assert docker._get_docker_path(path) == expected

0 commit comments

Comments
 (0)