Skip to content

Commit 3330569

Browse files
author
Viacheslav Boiko
committed
Merge upstream branch 'master' into feature/logs_since
Signed-off-by: Viacheslav Boiko <[email protected]>
2 parents 4a2db82 + 47ab89e commit 3330569

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+4239
-3365
lines changed

Makefile

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,35 @@ build:
1111
build-py3:
1212
docker build -t docker-py3 -f Dockerfile-py3 .
1313

14-
test: flake8 unit-test unit-test-py3 integration-dind
14+
build-dind-certs:
15+
docker build -t dpy-dind-certs -f tests/Dockerfile-dind-certs .
16+
17+
test: flake8 unit-test unit-test-py3 integration-dind integration-dind-ssl
1518

1619
unit-test: build
17-
docker run docker-py py.test tests/test.py tests/utils_test.py
20+
docker run docker-py py.test tests/unit
1821

1922
unit-test-py3: build-py3
20-
docker run docker-py3 py.test tests/test.py tests/utils_test.py
23+
docker run docker-py3 py.test tests/unit
2124

2225
integration-test: build
23-
docker run -v /var/run/docker.sock:/var/run/docker.sock docker-py py.test -rxs tests/integration_test.py
26+
docker run -v /var/run/docker.sock:/var/run/docker.sock docker-py py.test tests/integration
2427

2528
integration-test-py3: build-py3
26-
docker run -v /var/run/docker.sock:/var/run/docker.sock docker-py3 py.test -rxs tests/integration_test.py
29+
docker run -v /var/run/docker.sock:/var/run/docker.sock docker-py3 py.test tests/integration
2730

2831
integration-dind: build build-py3
29-
docker run -d --name dpy-dind --privileged dockerswarm/dind:1.8.1 docker -d -H tcp://0.0.0.0:2375
30-
docker run --env="DOCKER_HOST=tcp://docker:2375" --link=dpy-dind:docker docker-py py.test -rxs tests/integration_test.py
31-
docker run --env="DOCKER_HOST=tcp://docker:2375" --link=dpy-dind:docker docker-py3 py.test -rxs tests/integration_test.py
32+
docker run -d --name dpy-dind --env="DOCKER_HOST=tcp://localhost:2375" --privileged dockerswarm/dind:1.8.1 docker -d -H tcp://0.0.0.0:2375
33+
docker run --env="DOCKER_HOST=tcp://docker:2375" --link=dpy-dind:docker docker-py py.test tests/integration
34+
docker run --env="DOCKER_HOST=tcp://docker:2375" --link=dpy-dind:docker docker-py3 py.test tests/integration
3235
docker rm -vf dpy-dind
3336

37+
integration-dind-ssl: build-dind-certs build build-py3
38+
docker run -d --name dpy-dind-certs dpy-dind-certs
39+
docker run -d --env="DOCKER_HOST=tcp://localhost:2375" --env="DOCKER_TLS_VERIFY=1" --env="DOCKER_CERT_PATH=/certs" --volumes-from dpy-dind-certs --name dpy-dind-ssl -v /tmp --privileged dockerswarm/dind:1.8.1 docker daemon --tlsverify --tlscacert=/certs/ca.pem --tlscert=/certs/server-cert.pem --tlskey=/certs/server-key.pem -H tcp://0.0.0.0:2375
40+
docker run --volumes-from dpy-dind-ssl --env="DOCKER_HOST=tcp://docker:2375" --env="DOCKER_TLS_VERIFY=1" --env="DOCKER_CERT_PATH=/certs" --link=dpy-dind-ssl:docker docker-py py.test tests/integration_test.py
41+
docker run --volumes-from dpy-dind-ssl --env="DOCKER_HOST=tcp://docker:2375" --env="DOCKER_TLS_VERIFY=1" --env="DOCKER_CERT_PATH=/certs" --link=dpy-dind-ssl:docker docker-py3 py.test tests/integration_test.py
42+
docker rm -vf dpy-dind-ssl dpy-dind-certs
43+
3444
flake8: build
35-
docker run docker-py flake8 docker tests
45+
docker run docker-py flake8 docker tests

docker/api/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
from .exec_api import ExecApiMixin
66
from .image import ImageApiMixin
77
from .volume import VolumeApiMixin
8+
from .network import NetworkApiMixin

docker/api/container.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ def containers(self, quiet=False, all=False, trunc=False, latest=False,
7676

7777
@utils.check_resource
7878
def copy(self, container, resource):
79+
if utils.version_gte(self._version, '1.20'):
80+
warnings.warn(
81+
'Client.copy() is deprecated for API version >= 1.20, '
82+
'please use get_archive() instead',
83+
DeprecationWarning
84+
)
7985
res = self._post_json(
8086
self._url("/containers/{0}/copy".format(container)),
8187
data={"Resource": resource},
@@ -146,6 +152,21 @@ def export(self, container):
146152
self._raise_for_status(res)
147153
return res.raw
148154

155+
@utils.check_resource
156+
@utils.minimum_version('1.20')
157+
def get_archive(self, container, path):
158+
params = {
159+
'path': path
160+
}
161+
url = self._url('/containers/{0}/archive', container)
162+
res = self._get(url, params=params, stream=True)
163+
self._raise_for_status(res)
164+
encoded_stat = res.headers.get('x-docker-container-path-stat')
165+
return (
166+
res.raw,
167+
utils.decode_json_header(encoded_stat) if encoded_stat else None
168+
)
169+
149170
@utils.check_resource
150171
def inspect_container(self, container):
151172
return self._result(
@@ -226,6 +247,15 @@ def port(self, container, private_port):
226247

227248
return h_ports
228249

250+
@utils.check_resource
251+
@utils.minimum_version('1.20')
252+
def put_archive(self, container, path, data):
253+
params = {'path': path}
254+
url = self._url('/containers/{0}/archive', container)
255+
res = self._put(url, params=params, data=data)
256+
self._raise_for_status(res)
257+
return res.status_code == 200
258+
229259
@utils.check_resource
230260
def remove_container(self, container, v=False, link=False, force=False):
231261
params = {'v': v, 'link': link, 'force': force}

docker/api/exec_api.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import shlex
2-
31
import six
42

53
from .. import errors
@@ -20,7 +18,7 @@ def exec_create(self, container, cmd, stdout=True, stderr=True, tty=False,
2018
'User-specific exec is not supported in API < 1.19'
2119
)
2220
if isinstance(cmd, six.string_types):
23-
cmd = shlex.split(str(cmd))
21+
cmd = utils.split_command(cmd)
2422

2523
data = {
2624
'Container': container,

docker/api/network.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import json
2+
3+
from ..utils import check_resource, minimum_version
4+
5+
6+
class NetworkApiMixin(object):
7+
@minimum_version('1.21')
8+
def networks(self, names=None, ids=None):
9+
filters = {}
10+
if names:
11+
filters['name'] = names
12+
if ids:
13+
filters['id'] = ids
14+
15+
params = {'filters': json.dumps(filters)}
16+
17+
url = self._url("/networks")
18+
res = self._get(url, params=params)
19+
return self._result(res, json=True)
20+
21+
@minimum_version('1.21')
22+
def create_network(self, name, driver=None):
23+
data = {
24+
'name': name,
25+
'driver': driver,
26+
}
27+
url = self._url("/networks/create")
28+
res = self._post_json(url, data=data)
29+
return self._result(res, json=True)
30+
31+
@minimum_version('1.21')
32+
def remove_network(self, net_id):
33+
url = self._url("/networks/{0}", net_id)
34+
res = self._delete(url)
35+
self._raise_for_status(res)
36+
37+
@minimum_version('1.21')
38+
def inspect_network(self, net_id):
39+
url = self._url("/networks/{0}", net_id)
40+
res = self._get(url)
41+
return self._result(res, json=True)
42+
43+
@check_resource
44+
@minimum_version('1.21')
45+
def connect_container_to_network(self, container, net_id):
46+
data = {"container": container}
47+
url = self._url("/networks/{0}/connect", net_id)
48+
self._post_json(url, data=data)
49+
50+
@check_resource
51+
@minimum_version('1.21')
52+
def disconnect_container_from_network(self, container, net_id):
53+
data = {"container": container}
54+
url = self._url("/networks/{0}/disconnect", net_id)
55+
self._post_json(url, data=data)

docker/api/volume.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def volumes(self, filters=None):
1212

1313
@utils.minimum_version('1.21')
1414
def create_volume(self, name, driver=None, driver_opts=None):
15-
url = self._url('/volumes')
15+
url = self._url('/volumes/create')
1616
if driver_opts is not None and not isinstance(driver_opts, dict):
1717
raise TypeError('driver_opts must be a dictionary')
1818

docker/auth/auth.py

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

1515
import base64
16-
import fileinput
1716
import json
1817
import logging
1918
import os
@@ -102,7 +101,7 @@ def decode_auth(auth):
102101

103102
def encode_header(auth):
104103
auth_json = json.dumps(auth).encode('ascii')
105-
return base64.b64encode(auth_json)
104+
return base64.urlsafe_b64encode(auth_json)
106105

107106

108107
def parse_auth(entries):
@@ -132,78 +131,79 @@ def parse_auth(entries):
132131
return conf
133132

134133

134+
def find_config_file(config_path=None):
135+
environment_path = os.path.join(
136+
os.environ.get('DOCKER_CONFIG'),
137+
os.path.basename(DOCKER_CONFIG_FILENAME)
138+
) if os.environ.get('DOCKER_CONFIG') else None
139+
140+
paths = [
141+
config_path, # 1
142+
environment_path, # 2
143+
os.path.join(os.path.expanduser('~'), DOCKER_CONFIG_FILENAME), # 3
144+
os.path.join(
145+
os.path.expanduser('~'), LEGACY_DOCKER_CONFIG_FILENAME
146+
) # 4
147+
]
148+
149+
for path in paths:
150+
if path and os.path.exists(path):
151+
return path
152+
return None
153+
154+
135155
def load_config(config_path=None):
136156
"""
137157
Loads authentication data from a Docker configuration file in the given
138158
root directory or if config_path is passed use given path.
159+
Lookup priority:
160+
explicit config_path parameter > DOCKER_CONFIG environment variable >
161+
~/.docker/config.json > ~/.dockercfg
139162
"""
140-
conf = {}
141-
data = None
142-
143-
# Prefer ~/.docker/config.json.
144-
config_file = config_path or os.path.join(os.path.expanduser('~'),
145-
DOCKER_CONFIG_FILENAME)
146-
147-
log.debug("Trying {0}".format(config_file))
148-
149-
if os.path.exists(config_file):
150-
try:
151-
with open(config_file) as f:
152-
for section, data in six.iteritems(json.load(f)):
153-
if section != 'auths':
154-
continue
155-
log.debug("Found 'auths' section")
156-
return parse_auth(data)
157-
log.debug("Couldn't find 'auths' section")
158-
except (IOError, KeyError, ValueError) as e:
159-
# Likely missing new Docker config file or it's in an
160-
# unknown format, continue to attempt to read old location
161-
# and format.
162-
log.debug(e)
163-
pass
164-
else:
165-
log.debug("File doesn't exist")
166-
167-
config_file = config_path or os.path.join(os.path.expanduser('~'),
168-
LEGACY_DOCKER_CONFIG_FILENAME)
169163

170-
log.debug("Trying {0}".format(config_file))
164+
config_file = find_config_file(config_path)
171165

172-
if not os.path.exists(config_file):
173-
log.debug("File doesn't exist - returning empty config")
166+
if not config_file:
167+
log.debug("File doesn't exist")
174168
return {}
175169

176-
log.debug("Attempting to parse as JSON")
177170
try:
178171
with open(config_file) as f:
179-
return parse_auth(json.load(f))
180-
except Exception as e:
172+
data = json.load(f)
173+
if data.get('auths'):
174+
log.debug("Found 'auths' section")
175+
return parse_auth(data['auths'])
176+
else:
177+
log.debug("Couldn't find 'auths' section")
178+
f.seek(0)
179+
return parse_auth(json.load(f))
180+
except (IOError, KeyError, ValueError) as e:
181+
# Likely missing new Docker config file or it's in an
182+
# unknown format, continue to attempt to read old location
183+
# and format.
181184
log.debug(e)
182-
pass
183185

184-
# If that fails, we assume the configuration file contains a single
185-
# authentication token for the public registry in the following format:
186-
#
187-
# auth = AUTH_TOKEN
188-
189186
log.debug("Attempting to parse legacy auth file format")
190187
try:
191188
data = []
192-
for line in fileinput.input(config_file):
193-
data.append(line.strip().split(' = ')[1])
194-
if len(data) < 2:
195-
# Not enough data
196-
raise errors.InvalidConfigFile(
197-
'Invalid or empty configuration file!')
189+
with open(config_file) as f:
190+
for line in f.readlines():
191+
data.append(line.strip().split(' = ')[1])
192+
if len(data) < 2:
193+
# Not enough data
194+
raise errors.InvalidConfigFile(
195+
'Invalid or empty configuration file!'
196+
)
198197

199198
username, password = decode_auth(data[0])
200-
conf[INDEX_NAME] = {
201-
'username': username,
202-
'password': password,
203-
'email': data[1],
204-
'serveraddress': INDEX_URL,
199+
return {
200+
INDEX_NAME: {
201+
'username': username,
202+
'password': password,
203+
'email': data[1],
204+
'serveraddress': INDEX_URL,
205+
}
205206
}
206-
return conf
207207
except Exception as e:
208208
log.debug(e)
209209
pass

docker/client.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ class Client(
3939
api.DaemonApiMixin,
4040
api.ExecApiMixin,
4141
api.ImageApiMixin,
42-
api.VolumeApiMixin):
42+
api.VolumeApiMixin,
43+
api.NetworkApiMixin):
4344
def __init__(self, base_url=None, version=None,
4445
timeout=constants.DEFAULT_TIMEOUT_SECONDS, tls=False):
4546
super(Client, self).__init__()
@@ -108,6 +109,9 @@ def _post(self, url, **kwargs):
108109
def _get(self, url, **kwargs):
109110
return self.get(url, **self._set_request_timeout(kwargs))
110111

112+
def _put(self, url, **kwargs):
113+
return self.put(url, **self._set_request_timeout(kwargs))
114+
111115
def _delete(self, url, **kwargs):
112116
return self.delete(url, **self._set_request_timeout(kwargs))
113117

@@ -184,6 +188,8 @@ def _get_raw_response_socket(self, response):
184188
self._raise_for_status(response)
185189
if six.PY3:
186190
sock = response.raw._fp.fp.raw
191+
if self.base_url.startswith("https://"):
192+
sock = sock._sock
187193
else:
188194
sock = response.raw._fp.fp._sock
189195
try:
@@ -240,10 +246,7 @@ def _multiplexed_response_stream_helper(self, response):
240246
# Disable timeout on the underlying socket to prevent
241247
# Read timed out(s) for long running processes
242248
socket = self._get_raw_response_socket(response)
243-
if six.PY3:
244-
socket._sock.settimeout(None)
245-
else:
246-
socket.settimeout(None)
249+
self._disable_socket_timeout(socket)
247250

248251
while True:
249252
header = response.raw.read(constants.STREAM_HEADER_SIZE_BYTES)
@@ -272,6 +275,19 @@ def _stream_raw_result(self, response):
272275
for out in response.iter_content(chunk_size=1, decode_unicode=True):
273276
yield out
274277

278+
def _disable_socket_timeout(self, socket):
279+
""" Depending on the combination of python version and whether we're
280+
connecting over http or https, we might need to access _sock, which
281+
may or may not exist; or we may need to just settimeout on socket
282+
itself, which also may or may not have settimeout on it.
283+
284+
To avoid missing the correct one, we try both.
285+
"""
286+
if hasattr(socket, "settimeout"):
287+
socket.settimeout(None)
288+
if hasattr(socket, "_sock") and hasattr(socket._sock, "settimeout"):
289+
socket._sock.settimeout(None)
290+
275291
def _get_result(self, container, stream, res):
276292
cont = self.inspect_container(container)
277293
return self._get_result_tty(stream, res, cont['Config']['Tty'])

0 commit comments

Comments
 (0)