Skip to content

Commit 1e389b7

Browse files
authored
Merge pull request #2074 from docker/3.4.1-release
3.4.1 release
2 parents f70545e + bc28fd0 commit 1e389b7

File tree

11 files changed

+703
-623
lines changed

11 files changed

+703
-623
lines changed

docker/api/container.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,9 @@ def commit(self, container, repository=None, tag=None, message=None,
139139
'changes': changes
140140
}
141141
u = self._url("/commit")
142-
return self._result(self._post_json(u, data=conf, params=params),
143-
json=True)
142+
return self._result(
143+
self._post_json(u, data=conf, params=params), json=True
144+
)
144145

145146
def containers(self, quiet=False, all=False, trunc=False, latest=False,
146147
since=None, before=None, limit=-1, size=False,

docker/auth.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ def load_config(config_path=None, config_dict=None):
270270
"Couldn't find auth-related section ; attempting to interpret"
271271
"as auth-only file"
272272
)
273-
return parse_auth(config_dict)
273+
return {'auths': parse_auth(config_dict)}
274274

275275

276276
def _load_legacy_config(config_file):
@@ -287,14 +287,14 @@ def _load_legacy_config(config_file):
287287
)
288288

289289
username, password = decode_auth(data[0])
290-
return {
290+
return {'auths': {
291291
INDEX_NAME: {
292292
'username': username,
293293
'password': password,
294294
'email': data[1],
295295
'serveraddress': INDEX_URL,
296296
}
297-
}
297+
}}
298298
except Exception as e:
299299
log.debug(e)
300300
pass

docker/utils/build.py

Lines changed: 125 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import io
22
import os
33
import re
4-
import six
54
import tarfile
65
import tempfile
76

7+
import six
8+
9+
from .fnmatch import fnmatch
810
from ..constants import IS_WINDOWS_PLATFORM
9-
from fnmatch import fnmatch
10-
from itertools import chain
1111

1212

1313
_SEP = re.compile('/|\\\\') if IS_WINDOWS_PLATFORM else re.compile('/')
@@ -44,92 +44,9 @@ def exclude_paths(root, patterns, dockerfile=None):
4444
if dockerfile is None:
4545
dockerfile = 'Dockerfile'
4646

47-
def split_path(p):
48-
return [pt for pt in re.split(_SEP, p) if pt and pt != '.']
49-
50-
def normalize(p):
51-
# Leading and trailing slashes are not relevant. Yes,
52-
# "foo.py/" must exclude the "foo.py" regular file. "."
53-
# components are not relevant either, even if the whole
54-
# pattern is only ".", as the Docker reference states: "For
55-
# historical reasons, the pattern . is ignored."
56-
# ".." component must be cleared with the potential previous
57-
# component, regardless of whether it exists: "A preprocessing
58-
# step [...] eliminates . and .. elements using Go's
59-
# filepath.".
60-
i = 0
61-
split = split_path(p)
62-
while i < len(split):
63-
if split[i] == '..':
64-
del split[i]
65-
if i > 0:
66-
del split[i - 1]
67-
i -= 1
68-
else:
69-
i += 1
70-
return split
71-
72-
patterns = (
73-
(True, normalize(p[1:]))
74-
if p.startswith('!') else
75-
(False, normalize(p))
76-
for p in patterns)
77-
patterns = list(reversed(list(chain(
78-
# Exclude empty patterns such as "." or the empty string.
79-
filter(lambda p: p[1], patterns),
80-
# Always include the Dockerfile and .dockerignore
81-
[(True, split_path(dockerfile)), (True, ['.dockerignore'])]))))
82-
return set(walk(root, patterns))
83-
84-
85-
def walk(root, patterns, default=True):
86-
"""
87-
A collection of file lying below root that should be included according to
88-
patterns.
89-
"""
90-
91-
def match(p):
92-
if p[1][0] == '**':
93-
rec = (p[0], p[1][1:])
94-
return [p] + (match(rec) if rec[1] else [rec])
95-
elif fnmatch(f, p[1][0]):
96-
return [(p[0], p[1][1:])]
97-
else:
98-
return []
99-
100-
for f in os.listdir(root):
101-
cur = os.path.join(root, f)
102-
# The patterns if recursing in that directory.
103-
sub = list(chain(*(match(p) for p in patterns)))
104-
# Whether this file is explicitely included / excluded.
105-
hit = next((p[0] for p in sub if not p[1]), None)
106-
# Whether this file is implicitely included / excluded.
107-
matched = default if hit is None else hit
108-
sub = list(filter(lambda p: p[1], sub))
109-
if os.path.isdir(cur) and not os.path.islink(cur):
110-
# Entirely skip directories if there are no chance any subfile will
111-
# be included.
112-
if all(not p[0] for p in sub) and not matched:
113-
continue
114-
# I think this would greatly speed up dockerignore handling by not
115-
# recursing into directories we are sure would be entirely
116-
# included, and only yielding the directory itself, which will be
117-
# recursively archived anyway. However the current unit test expect
118-
# the full list of subfiles and I'm not 100% sure it would make no
119-
# difference yet.
120-
# if all(p[0] for p in sub) and matched:
121-
# yield f
122-
# continue
123-
children = False
124-
for r in (os.path.join(f, p) for p in walk(cur, sub, matched)):
125-
yield r
126-
children = True
127-
# The current unit tests expect directories only under those
128-
# conditions. It might be simplifiable though.
129-
if (not sub or not children) and hit or hit is None and default:
130-
yield f
131-
elif matched:
132-
yield f
47+
patterns.append('!' + dockerfile)
48+
pm = PatternMatcher(patterns)
49+
return set(pm.walk(root))
13350

13451

13552
def build_file_list(root):
@@ -217,3 +134,122 @@ def mkbuildcontext(dockerfile):
217134
t.close()
218135
f.seek(0)
219136
return f
137+
138+
139+
def split_path(p):
140+
return [pt for pt in re.split(_SEP, p) if pt and pt != '.']
141+
142+
143+
def normalize_slashes(p):
144+
if IS_WINDOWS_PLATFORM:
145+
return '/'.join(split_path(p))
146+
return p
147+
148+
149+
def walk(root, patterns, default=True):
150+
pm = PatternMatcher(patterns)
151+
return pm.walk(root)
152+
153+
154+
# Heavily based on
155+
# https://github.com/moby/moby/blob/master/pkg/fileutils/fileutils.go
156+
class PatternMatcher(object):
157+
def __init__(self, patterns):
158+
self.patterns = list(filter(
159+
lambda p: p.dirs, [Pattern(p) for p in patterns]
160+
))
161+
self.patterns.append(Pattern('!.dockerignore'))
162+
163+
def matches(self, filepath):
164+
matched = False
165+
parent_path = os.path.dirname(filepath)
166+
parent_path_dirs = split_path(parent_path)
167+
168+
for pattern in self.patterns:
169+
negative = pattern.exclusion
170+
match = pattern.match(filepath)
171+
if not match and parent_path != '':
172+
if len(pattern.dirs) <= len(parent_path_dirs):
173+
match = pattern.match(
174+
os.path.sep.join(parent_path_dirs[:len(pattern.dirs)])
175+
)
176+
177+
if match:
178+
matched = not negative
179+
180+
return matched
181+
182+
def walk(self, root):
183+
def rec_walk(current_dir):
184+
for f in os.listdir(current_dir):
185+
fpath = os.path.join(
186+
os.path.relpath(current_dir, root), f
187+
)
188+
if fpath.startswith('.' + os.path.sep):
189+
fpath = fpath[2:]
190+
match = self.matches(fpath)
191+
if not match:
192+
yield fpath
193+
194+
cur = os.path.join(root, fpath)
195+
if not os.path.isdir(cur) or os.path.islink(cur):
196+
continue
197+
198+
if match:
199+
# If we want to skip this file and it's a directory
200+
# then we should first check to see if there's an
201+
# excludes pattern (e.g. !dir/file) that starts with this
202+
# dir. If so then we can't skip this dir.
203+
skip = True
204+
205+
for pat in self.patterns:
206+
if not pat.exclusion:
207+
continue
208+
if pat.cleaned_pattern.startswith(
209+
normalize_slashes(fpath)):
210+
skip = False
211+
break
212+
if skip:
213+
continue
214+
for sub in rec_walk(cur):
215+
yield sub
216+
217+
return rec_walk(root)
218+
219+
220+
class Pattern(object):
221+
def __init__(self, pattern_str):
222+
self.exclusion = False
223+
if pattern_str.startswith('!'):
224+
self.exclusion = True
225+
pattern_str = pattern_str[1:]
226+
227+
self.dirs = self.normalize(pattern_str)
228+
self.cleaned_pattern = '/'.join(self.dirs)
229+
230+
@classmethod
231+
def normalize(cls, p):
232+
233+
# Leading and trailing slashes are not relevant. Yes,
234+
# "foo.py/" must exclude the "foo.py" regular file. "."
235+
# components are not relevant either, even if the whole
236+
# pattern is only ".", as the Docker reference states: "For
237+
# historical reasons, the pattern . is ignored."
238+
# ".." component must be cleared with the potential previous
239+
# component, regardless of whether it exists: "A preprocessing
240+
# step [...] eliminates . and .. elements using Go's
241+
# filepath.".
242+
i = 0
243+
split = split_path(p)
244+
while i < len(split):
245+
if split[i] == '..':
246+
del split[i]
247+
if i > 0:
248+
del split[i - 1]
249+
i -= 1
250+
else:
251+
i += 1
252+
return split
253+
254+
def match(self, filepath):
255+
return fnmatch(normalize_slashes(filepath), self.cleaned_pattern)

docker/utils/fnmatch.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,5 @@ def translate(pat):
111111
res = '%s[%s]' % (res, stuff)
112112
else:
113113
res = res + re.escape(c)
114+
114115
return res + '$'

docker/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
version = "3.4.0"
1+
version = "3.4.1"
22
version_info = tuple([int(d) for d in version.split("-")[0].split(".")])

docs/change-log.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
Change log
22
==========
33

4+
3.4.1
5+
-----
6+
7+
[List of PRs / issues for this release](https://github.com/docker/docker-py/milestone/52?closed=1)
8+
9+
### Bugfixes
10+
11+
* Fixed a bug that caused auth values in config files written using one of the
12+
legacy formats to be ignored
13+
* Fixed issues with handling of double-wildcard `**` patterns in
14+
`.dockerignore` files
15+
416
3.4.0
517
-----
618

tests/helpers.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,12 @@ def assert_cat_socket_detached_with_keys(sock, inputs):
123123
sock.sendall(b'make sure the socket is closed\n')
124124
else:
125125
sock.sendall(b"make sure the socket is closed\n")
126-
assert sock.recv(32) == b''
126+
data = sock.recv(128)
127+
# New in 18.06: error message is broadcast over the socket when reading
128+
# after detach
129+
assert data == b'' or data.startswith(
130+
b'exec attach failed: error on attach stdin: read escape sequence'
131+
)
127132

128133

129134
def ctrl_with(char):

tests/integration/api_client_test.py

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
import base64
2-
import os
3-
import tempfile
41
import time
52
import unittest
63
import warnings
@@ -24,43 +21,6 @@ def test_info(self):
2421
assert 'Debug' in res
2522

2623

27-
class LoadConfigTest(BaseAPIIntegrationTest):
28-
def test_load_legacy_config(self):
29-
folder = tempfile.mkdtemp()
30-
self.tmp_folders.append(folder)
31-
cfg_path = os.path.join(folder, '.dockercfg')
32-
f = open(cfg_path, 'w')
33-
auth_ = base64.b64encode(b'sakuya:izayoi').decode('ascii')
34-
f.write('auth = {0}\n'.format(auth_))
35-
f.write('email = [email protected]')
36-
f.close()
37-
cfg = docker.auth.load_config(cfg_path)
38-
assert cfg[docker.auth.INDEX_NAME] is not None
39-
cfg = cfg[docker.auth.INDEX_NAME]
40-
assert cfg['username'] == 'sakuya'
41-
assert cfg['password'] == 'izayoi'
42-
assert cfg['email'] == '[email protected]'
43-
assert cfg.get('Auth') is None
44-
45-
def test_load_json_config(self):
46-
folder = tempfile.mkdtemp()
47-
self.tmp_folders.append(folder)
48-
cfg_path = os.path.join(folder, '.dockercfg')
49-
f = open(os.path.join(folder, '.dockercfg'), 'w')
50-
auth_ = base64.b64encode(b'sakuya:izayoi').decode('ascii')
51-
email_ = '[email protected]'
52-
f.write('{{"{0}": {{"auth": "{1}", "email": "{2}"}}}}\n'.format(
53-
docker.auth.INDEX_URL, auth_, email_))
54-
f.close()
55-
cfg = docker.auth.load_config(cfg_path)
56-
assert cfg[docker.auth.INDEX_URL] is not None
57-
cfg = cfg[docker.auth.INDEX_URL]
58-
assert cfg['username'] == 'sakuya'
59-
assert cfg['password'] == 'izayoi'
60-
assert cfg['email'] == '[email protected]'
61-
assert cfg.get('Auth') is None
62-
63-
6424
class AutoDetectVersionTest(unittest.TestCase):
6525
def test_client_init(self):
6626
client = docker.APIClient(version='auto', **kwargs_from_env())

0 commit comments

Comments
 (0)