mirror of
https://github.com/pre-commit/pre-commit.git
synced 2026-02-17 08:14:42 +04:00
fix: fix path mounting when running in Docker
Currently pre-commit mounts the current directory to /src and uses current directory name as mount base. However this does not work when pre-commit is run inside the container on some mounted path already, because mount points are relative to the host, not to the container. Fixes #1387
This commit is contained in:
parent
52e1dd6099
commit
6d5d386c9f
2 changed files with 176 additions and 4 deletions
|
|
@ -1,5 +1,7 @@
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
|
import socket
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
|
|
@ -8,6 +10,7 @@ from pre_commit.hook import Hook
|
||||||
from pre_commit.languages import helpers
|
from pre_commit.languages import helpers
|
||||||
from pre_commit.prefix import Prefix
|
from pre_commit.prefix import Prefix
|
||||||
from pre_commit.util import clean_path_on_failure
|
from pre_commit.util import clean_path_on_failure
|
||||||
|
from pre_commit.util import cmd_output_b
|
||||||
|
|
||||||
ENVIRONMENT_DIR = 'docker'
|
ENVIRONMENT_DIR = 'docker'
|
||||||
PRE_COMMIT_LABEL = 'PRE_COMMIT'
|
PRE_COMMIT_LABEL = 'PRE_COMMIT'
|
||||||
|
|
@ -15,6 +18,34 @@ get_default_version = helpers.basic_get_default_version
|
||||||
healthy = helpers.basic_healthy
|
healthy = helpers.basic_healthy
|
||||||
|
|
||||||
|
|
||||||
|
def _is_in_docker() -> bool:
|
||||||
|
try:
|
||||||
|
with open('/proc/1/cgroup', 'rb') as f:
|
||||||
|
return b'docker' in f.read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
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, = json.loads(out)
|
||||||
|
for mount in container['Mounts']:
|
||||||
|
src_path = mount['Source']
|
||||||
|
to_path = mount['Destination']
|
||||||
|
if os.path.commonpath((path, to_path)) == to_path:
|
||||||
|
# So there is something in common,
|
||||||
|
# and we can proceed remapping it
|
||||||
|
return path.replace(to_path, src_path)
|
||||||
|
# we're in Docker, but the path is not mounted, cannot really do anything,
|
||||||
|
# so fall back to original path
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
def md5(s: str) -> str: # pragma: win32 no cover
|
def md5(s: str) -> str: # pragma: win32 no cover
|
||||||
return hashlib.md5(s.encode()).hexdigest()
|
return hashlib.md5(s.encode()).hexdigest()
|
||||||
|
|
||||||
|
|
@ -73,7 +104,7 @@ def docker_cmd() -> Tuple[str, ...]: # pragma: win32 no cover
|
||||||
# https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from
|
# https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from
|
||||||
# The `Z` option tells Docker to label the content with a private
|
# The `Z` option tells Docker to label the content with a private
|
||||||
# unshared label. Only the current container can use a private volume.
|
# unshared label. Only the current container can use a private volume.
|
||||||
'-v', f'{os.getcwd()}:/src:rw,Z',
|
'-v', f'{_get_docker_path(os.getcwd())}:/src:rw,Z',
|
||||||
'--workdir', '/src',
|
'--workdir', '/src',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,155 @@
|
||||||
|
import builtins
|
||||||
|
import json
|
||||||
|
import ntpath
|
||||||
|
import os.path
|
||||||
|
import posixpath
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from pre_commit.languages import docker
|
from pre_commit.languages import docker
|
||||||
|
|
||||||
|
|
||||||
def test_docker_fallback_user():
|
def test_docker_fallback_user():
|
||||||
def invalid_attribute():
|
def invalid_attribute():
|
||||||
raise AttributeError
|
raise AttributeError
|
||||||
|
|
||||||
with mock.patch.multiple(
|
with mock.patch.multiple(
|
||||||
'os', create=True,
|
'os', create=True,
|
||||||
getuid=invalid_attribute,
|
getuid=invalid_attribute,
|
||||||
getgid=invalid_attribute,
|
getgid=invalid_attribute,
|
||||||
):
|
):
|
||||||
assert docker.get_docker_user() == ()
|
assert docker.get_docker_user() == ()
|
||||||
|
|
||||||
|
|
||||||
|
def test_in_docker_no_file():
|
||||||
|
with mock.patch.object(builtins, 'open', side_effect=FileNotFoundError):
|
||||||
|
assert docker._is_in_docker() is False
|
||||||
|
|
||||||
|
|
||||||
|
def _mock_open(data):
|
||||||
|
return mock.patch.object(
|
||||||
|
builtins,
|
||||||
|
'open',
|
||||||
|
new_callable=mock.mock_open,
|
||||||
|
read_data=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):
|
||||||
|
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):
|
||||||
|
assert docker._is_in_docker() is False
|
||||||
|
|
||||||
|
|
||||||
|
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'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def in_docker():
|
||||||
|
with mock.patch.object(docker, '_is_in_docker', return_value=True):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def _linux_commonpath():
|
||||||
|
return mock.patch.object(os.path, 'commonpath', posixpath.commonpath)
|
||||||
|
|
||||||
|
|
||||||
|
def _nt_commonpath():
|
||||||
|
return mock.patch.object(os.path, 'commonpath', ntpath.commonpath)
|
||||||
|
|
||||||
|
|
||||||
|
def _docker_output(out):
|
||||||
|
ret = (0, out, b'')
|
||||||
|
return mock.patch.object(docker, 'cmd_output_b', return_value=ret)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_docker_path_in_docker_no_binds_same_path(in_docker):
|
||||||
|
docker_out = json.dumps([{'Mounts': []}]).encode()
|
||||||
|
|
||||||
|
with _docker_output(docker_out):
|
||||||
|
assert docker._get_docker_path('abc') == 'abc'
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_docker_path_in_docker_binds_path_equal(in_docker):
|
||||||
|
binds_list = [{'Source': '/opt/my_code', 'Destination': '/project'}]
|
||||||
|
docker_out = json.dumps([{'Mounts': binds_list}]).encode()
|
||||||
|
|
||||||
|
with _linux_commonpath(), _docker_output(docker_out):
|
||||||
|
assert docker._get_docker_path('/project') == '/opt/my_code'
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_docker_path_in_docker_binds_path_complex(in_docker):
|
||||||
|
binds_list = [{'Source': '/opt/my_code', 'Destination': '/project'}]
|
||||||
|
docker_out = json.dumps([{'Mounts': binds_list}]).encode()
|
||||||
|
|
||||||
|
with _linux_commonpath(), _docker_output(docker_out):
|
||||||
|
path = '/project/test/something'
|
||||||
|
assert docker._get_docker_path(path) == '/opt/my_code/test/something'
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_docker_path_in_docker_no_substring(in_docker):
|
||||||
|
binds_list = [{'Source': '/opt/my_code', 'Destination': '/project'}]
|
||||||
|
docker_out = json.dumps([{'Mounts': binds_list}]).encode()
|
||||||
|
|
||||||
|
with _linux_commonpath(), _docker_output(docker_out):
|
||||||
|
path = '/projectSuffix/test/something'
|
||||||
|
assert docker._get_docker_path(path) == path
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_docker_path_in_docker_binds_path_many_binds(in_docker):
|
||||||
|
binds_list = [
|
||||||
|
{'Source': '/something_random', 'Destination': '/not-related'},
|
||||||
|
{'Source': '/opt/my_code', 'Destination': '/project'},
|
||||||
|
{'Source': '/something-random-2', 'Destination': '/not-related-2'},
|
||||||
|
]
|
||||||
|
docker_out = json.dumps([{'Mounts': binds_list}]).encode()
|
||||||
|
|
||||||
|
with _linux_commonpath(), _docker_output(docker_out):
|
||||||
|
assert docker._get_docker_path('/project') == '/opt/my_code'
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_docker_path_in_docker_windows(in_docker):
|
||||||
|
binds_list = [{'Source': r'c:\users\user', 'Destination': r'c:\folder'}]
|
||||||
|
docker_out = json.dumps([{'Mounts': binds_list}]).encode()
|
||||||
|
|
||||||
|
with _nt_commonpath(), _docker_output(docker_out):
|
||||||
|
path = r'c:\folder\test\something'
|
||||||
|
expected = r'c:\users\user\test\something'
|
||||||
|
assert docker._get_docker_path(path) == expected
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue