Skip to content

Commit 8e45264

Browse files
committed
Merge pull request #268 from brutasse/265-dockerignore
Add support for .dockerignore
2 parents 9e39672 + 87b4d32 commit 8e45264

File tree

4 files changed

+120
-4
lines changed

4 files changed

+120
-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
@@ -356,7 +357,12 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None,
356357
'git://', 'github.com/')):
357358
remote = path
358359
else:
359-
context = utils.tar(path)
360+
dockerignore = os.path.join(path, '.dockerignore')
361+
exclude = None
362+
if os.path.exists(dockerignore):
363+
with open(dockerignore, 'r') as f:
364+
exclude = list(filter(bool, f.read().split('\n')))
365+
context = utils.tar(path, exclude=exclude)
360366

361367
if utils.compare_version('1.8', self._version) >= 0:
362368
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
@@ -47,10 +49,29 @@ def mkbuildcontext(dockerfile):
4749
return f
4850

4951

50-
def tar(path):
52+
def fnmatch_any(relpath, patterns):
53+
return any([fnmatch(relpath, pattern) for pattern in patterns])
54+
55+
56+
def tar(path, exclude=None):
5157
f = tempfile.NamedTemporaryFile()
5258
t = tarfile.open(mode='w', fileobj=f)
53-
t.add(path, arcname='.')
59+
for dirpath, dirnames, filenames in os.walk(path):
60+
relpath = os.path.relpath(dirpath, path)
61+
if relpath == '.':
62+
relpath = ''
63+
if exclude is None:
64+
fnames = filenames
65+
else:
66+
dirnames[:] = [d for d in dirnames
67+
if not fnmatch_any(os.path.join(relpath, d),
68+
exclude)]
69+
fnames = [name for name in filenames
70+
if not fnmatch_any(os.path.join(relpath, name),
71+
exclude)]
72+
for name in fnames:
73+
arcname = os.path.join(relpath, name)
74+
t.add(os.path.join(path, arcname), arcname=arcname)
5475
t.close()
5576
f.seek(0)
5677
return f

tests/integration_test.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import docker
2626
import six
2727

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

@@ -828,6 +830,43 @@ def runTest(self):
828830
self.assertEqual(logs.find('HTTP code: 403'), -1)
829831

830832

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

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)