Skip to content

Commit

Permalink
Merge pull request #1888 from okainov/docker-fix-2
Browse files Browse the repository at this point in the history
fix: fix path mounting when running in Docker
  • Loading branch information
asottile committed Apr 29, 2021
2 parents 52e1dd6 + 6d5d386 commit 7f13fa5
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 4 deletions.
33 changes: 32 additions & 1 deletion pre_commit/languages/docker.py
@@ -1,5 +1,7 @@
import hashlib
import json
import os
import socket
from typing import Sequence
from typing import Tuple

Expand All @@ -8,13 +10,42 @@
from pre_commit.languages import helpers
from pre_commit.prefix import Prefix
from pre_commit.util import clean_path_on_failure
from pre_commit.util import cmd_output_b

ENVIRONMENT_DIR = 'docker'
PRE_COMMIT_LABEL = 'PRE_COMMIT'
get_default_version = helpers.basic_get_default_version
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
return hashlib.md5(s.encode()).hexdigest()

Expand Down Expand Up @@ -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
# The `Z` option tells Docker to label the content with a private
# 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',
)

Expand Down
147 changes: 144 additions & 3 deletions tests/languages/docker_test.py
@@ -1,14 +1,155 @@
import builtins
import json
import ntpath
import os.path
import posixpath
from unittest import mock

import pytest

from pre_commit.languages import docker


def test_docker_fallback_user():
def invalid_attribute():
raise AttributeError

with mock.patch.multiple(
'os', create=True,
getuid=invalid_attribute,
getgid=invalid_attribute,
'os', create=True,
getuid=invalid_attribute,
getgid=invalid_attribute,
):
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

0 comments on commit 7f13fa5

Please sign in to comment.