Skip to content

Commit 87b4d32

Browse files
committed
Add support for .dockerignore
Fixes #265. Implementation is a bit more elaborate than docker's implementation and matches with the one proposed in moby/moby#6869 to handle permission issues more nicely.
1 parent 9170219 commit 87b4d32

File tree

4 files changed

+121
-4
lines changed

4 files changed

+121
-4
lines changed

docker/client.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
import json
16+
import os
1617
import re
1718
import shlex
1819
import struct
@@ -351,7 +352,12 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None,
351352
'git://', 'github.com/')):
352353
remote = path
353354
else:
354-
context = utils.tar(path)
355+
dockerignore = os.path.join(path, '.dockerignore')
356+
exclude = None
357+
if os.path.exists(dockerignore):
358+
with open(dockerignore, 'r') as f:
359+
exclude = list(filter(bool, f.read().split('\n')))
360+
context = utils.tar(path, exclude=exclude)
355361

356362
if utils.compare_version('1.8', self._version) >= 0:
357363
stream = True

docker/utils/utils.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
# limitations under the License.
1414

1515
import io
16+
import os
1617
import tarfile
1718
import tempfile
1819
from distutils.version import StrictVersion
20+
from fnmatch import fnmatch
1921

2022
import requests
2123
import six
@@ -42,10 +44,29 @@ def mkbuildcontext(dockerfile):
4244
return f
4345

4446

45-
def tar(path):
47+
def fnmatch_any(relpath, patterns):
48+
return any([fnmatch(relpath, pattern) for pattern in patterns])
49+
50+
51+
def tar(path, exclude=None):
4652
f = tempfile.NamedTemporaryFile()
4753
t = tarfile.open(mode='w', fileobj=f)
48-
t.add(path, arcname='.')
54+
for dirpath, dirnames, filenames in os.walk(path):
55+
relpath = os.path.relpath(dirpath, path)
56+
if relpath == '.':
57+
relpath = ''
58+
if exclude is None:
59+
fnames = filenames
60+
else:
61+
dirnames[:] = [d for d in dirnames
62+
if not fnmatch_any(os.path.join(relpath, d),
63+
exclude)]
64+
fnames = [name for name in filenames
65+
if not fnmatch_any(os.path.join(relpath, name),
66+
exclude)]
67+
for name in fnames:
68+
arcname = os.path.join(relpath, name)
69+
t.add(os.path.join(path, arcname), arcname=arcname)
4970
t.close()
5071
f.seek(0)
5172
return f

tests/integration_test.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@
1717
import json
1818
import io
1919
import os
20+
import shutil
2021
import signal
2122
import tempfile
2223
import unittest
2324

2425
import docker
2526
import six
2627

28+
from tests.test import Cleanup
29+
2730
# FIXME: missing tests for
2831
# export; history; import_image; insert; port; push; tag; get; load
2932

@@ -820,6 +823,43 @@ def runTest(self):
820823
self.assertEqual(logs.find('HTTP code: 403'), -1)
821824

822825

826+
class TestBuildWithDockerignore(Cleanup, BaseTestCase):
827+
def runTest(self):
828+
if self.client._version < 1.8:
829+
return
830+
831+
base_dir = tempfile.mkdtemp()
832+
self.addCleanup(shutil.rmtree, base_dir)
833+
834+
with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f:
835+
f.write("\n".join([
836+
'FROM busybox',
837+
'MAINTAINER docker-py',
838+
'ADD . /test',
839+
'RUN ls -A /test',
840+
]))
841+
842+
with open(os.path.join(base_dir, '.dockerignore'), 'w') as f:
843+
f.write("\n".join([
844+
'node_modules',
845+
'', # empty line
846+
]))
847+
848+
with open(os.path.join(base_dir, 'not-ignored'), 'w') as f:
849+
f.write("this file should not be ignored")
850+
851+
subdir = os.path.join(base_dir, 'node_modules', 'grunt-cli')
852+
os.makedirs(subdir)
853+
with open(os.path.join(subdir, 'grunt'), 'w') as f:
854+
f.write("grunt")
855+
856+
stream = self.client.build(path=base_dir, stream=True)
857+
logs = ''
858+
for chunk in stream:
859+
logs += chunk
860+
self.assertFalse('node_modules' in logs)
861+
self.assertTrue('not-ignored' in logs)
862+
823863
#######################
824864
# PY SPECIFIC TESTS #
825865
#######################

tests/test.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
import io
1818
import json
1919
import os
20+
import shutil
2021
import signal
22+
import sys
23+
import tarfile
2124
import tempfile
2225
import unittest
2326
import gzip
@@ -58,9 +61,34 @@ def fake_resp(url, data=None, **kwargs):
5861
docker.client.DEFAULT_DOCKER_API_VERSION)
5962

6063

64+
class Cleanup(object):
65+
if sys.version_info < (2, 7):
66+
# Provide a basic implementation of addCleanup for Python < 2.7
67+
def __init__(self, *args, **kwargs):
68+
super(Cleanup, self).__init__(*args, **kwargs)
69+
self._cleanups = []
70+
71+
def tearDown(self):
72+
super(Cleanup, self).tearDown()
73+
ok = True
74+
while self._cleanups:
75+
fn, args, kwargs = self._cleanups.pop(-1)
76+
try:
77+
fn(*args, **kwargs)
78+
except KeyboardInterrupt:
79+
raise
80+
except:
81+
ok = False
82+
if not ok:
83+
raise
84+
85+
def addCleanup(self, function, *args, **kwargs):
86+
self._cleanups.append((function, args, kwargs))
87+
88+
6189
@mock.patch.multiple('docker.Client', get=fake_request, post=fake_request,
6290
put=fake_request, delete=fake_request)
63-
class DockerClientTest(unittest.TestCase):
91+
class DockerClientTest(Cleanup, unittest.TestCase):
6492
def setUp(self):
6593
self.client = docker.Client()
6694
# Force-clear authconfig to avoid tampering with the tests
@@ -1350,11 +1378,13 @@ def test_build_container_custom_context_gzip(self):
13501378

13511379
def test_load_config_no_file(self):
13521380
folder = tempfile.mkdtemp()
1381+
self.addCleanup(shutil.rmtree, folder)
13531382
cfg = docker.auth.load_config(folder)
13541383
self.assertTrue(cfg is not None)
13551384

13561385
def test_load_config(self):
13571386
folder = tempfile.mkdtemp()
1387+
self.addCleanup(shutil.rmtree, folder)
13581388
f = open(os.path.join(folder, '.dockercfg'), 'w')
13591389
auth_ = base64.b64encode(b'sakuya:izayoi').decode('ascii')
13601390
f.write('auth = {0}\n'.format(auth_))
@@ -1369,6 +1399,26 @@ def test_load_config(self):
13691399
self.assertEqual(cfg['email'], '[email protected]')
13701400
self.assertEqual(cfg.get('auth'), None)
13711401

1402+
def test_tar_with_excludes(self):
1403+
base = tempfile.mkdtemp()
1404+
self.addCleanup(shutil.rmtree, base)
1405+
for d in ['test/foo', 'bar']:
1406+
os.makedirs(os.path.join(base, d))
1407+
for f in ['a.txt', 'b.py', 'other.png']:
1408+
with open(os.path.join(base, d, f), 'w') as f:
1409+
f.write("content")
1410+
1411+
for exclude, names in (
1412+
(['*.py'], ['bar/a.txt', 'bar/other.png',
1413+
'test/foo/a.txt', 'test/foo/other.png']),
1414+
(['*.png', 'bar'], ['test/foo/a.txt', 'test/foo/b.py']),
1415+
(['test/foo', 'a.txt'], ['bar/a.txt', 'bar/b.py',
1416+
'bar/other.png']),
1417+
):
1418+
archive = docker.utils.tar(base, exclude=exclude)
1419+
tar = tarfile.open(fileobj=archive)
1420+
self.assertEqual(sorted(tar.getnames()), names)
1421+
13721422

13731423
if __name__ == '__main__':
13741424
unittest.main()

0 commit comments

Comments
 (0)