Skip to content

Commit 79f27c6

Browse files
authored
Merge pull request #1921 from docker/3.1.0-release
3.1.0 release
2 parents c9ee022 + 1d85818 commit 79f27c6

26 files changed

+415
-214
lines changed

docker/api/client.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -350,10 +350,10 @@ def _multiplexed_response_stream_helper(self, response):
350350
break
351351
yield data
352352

353-
def _stream_raw_result(self, response):
354-
''' Stream result for TTY-enabled container '''
353+
def _stream_raw_result(self, response, chunk_size=1, decode=True):
354+
''' Stream result for TTY-enabled container and raw binary data'''
355355
self._raise_for_status(response)
356-
for out in response.iter_content(chunk_size=1, decode_unicode=True):
356+
for out in response.iter_content(chunk_size, decode):
357357
yield out
358358

359359
def _read_from_socket(self, response, stream, tty=False):

docker/api/container.py

+13-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from .. import errors
55
from .. import utils
6+
from ..constants import DEFAULT_DATA_CHUNK_SIZE
67
from ..types import (
78
ContainerConfig, EndpointConfig, HostConfig, NetworkingConfig
89
)
@@ -438,6 +439,8 @@ def create_host_config(self, *args, **kwargs):
438439
``0,1``).
439440
cpuset_mems (str): Memory nodes (MEMs) in which to allow execution
440441
(``0-3``, ``0,1``). Only effective on NUMA systems.
442+
device_cgroup_rules (:py:class:`list`): A list of cgroup rules to
443+
apply to the container.
441444
device_read_bps: Limit read rate (bytes per second) from a device
442445
in the form of: `[{"Path": "device_path", "Rate": rate}]`
443446
device_read_iops: Limit read rate (IO per second) from a device.
@@ -643,12 +646,15 @@ def diff(self, container):
643646
)
644647

645648
@utils.check_resource('container')
646-
def export(self, container):
649+
def export(self, container, chunk_size=DEFAULT_DATA_CHUNK_SIZE):
647650
"""
648651
Export the contents of a filesystem as a tar archive.
649652
650653
Args:
651654
container (str): The container to export
655+
chunk_size (int): The number of bytes returned by each iteration
656+
of the generator. If ``None``, data will be streamed as it is
657+
received. Default: 2 MB
652658
653659
Returns:
654660
(generator): The archived filesystem data stream
@@ -660,17 +666,20 @@ def export(self, container):
660666
res = self._get(
661667
self._url("/containers/{0}/export", container), stream=True
662668
)
663-
return self._stream_raw_result(res)
669+
return self._stream_raw_result(res, chunk_size, False)
664670

665671
@utils.check_resource('container')
666-
def get_archive(self, container, path):
672+
def get_archive(self, container, path, chunk_size=DEFAULT_DATA_CHUNK_SIZE):
667673
"""
668674
Retrieve a file or folder from a container in the form of a tar
669675
archive.
670676
671677
Args:
672678
container (str): The container where the file is located
673679
path (str): Path to the file or folder to retrieve
680+
chunk_size (int): The number of bytes returned by each iteration
681+
of the generator. If ``None``, data will be streamed as it is
682+
received. Default: 2 MB
674683
675684
Returns:
676685
(tuple): First element is a raw tar data stream. Second element is
@@ -688,7 +697,7 @@ def get_archive(self, container, path):
688697
self._raise_for_status(res)
689698
encoded_stat = res.headers.get('x-docker-container-path-stat')
690699
return (
691-
self._stream_raw_result(res),
700+
self._stream_raw_result(res, chunk_size, False),
692701
utils.decode_json_header(encoded_stat) if encoded_stat else None
693702
)
694703

docker/api/image.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,23 @@
44
import six
55

66
from .. import auth, errors, utils
7+
from ..constants import DEFAULT_DATA_CHUNK_SIZE
78

89
log = logging.getLogger(__name__)
910

1011

1112
class ImageApiMixin(object):
1213

1314
@utils.check_resource('image')
14-
def get_image(self, image):
15+
def get_image(self, image, chunk_size=DEFAULT_DATA_CHUNK_SIZE):
1516
"""
1617
Get a tarball of an image. Similar to the ``docker save`` command.
1718
1819
Args:
1920
image (str): Image name to get
21+
chunk_size (int): The number of bytes returned by each iteration
22+
of the generator. If ``None``, data will be streamed as it is
23+
received. Default: 2 MB
2024
2125
Returns:
2226
(generator): A stream of raw archive data.
@@ -34,7 +38,7 @@ def get_image(self, image):
3438
>>> f.close()
3539
"""
3640
res = self._get(self._url("/images/{0}/get", image), stream=True)
37-
return self._stream_raw_result(res)
41+
return self._stream_raw_result(res, chunk_size, False)
3842

3943
@utils.check_resource('image')
4044
def history(self, image):

docker/api/service.py

+5
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ def raise_version_error(param, min_version):
7373
if container_spec.get('Isolation') is not None:
7474
raise_version_error('ContainerSpec.isolation', '1.35')
7575

76+
if task_template.get('Resources'):
77+
if utils.version_lt(version, '1.32'):
78+
if task_template['Resources'].get('GenericResources'):
79+
raise_version_error('Resources.generic_resources', '1.32')
80+
7681

7782
def _merge_task_template(current, override):
7883
merged = current.copy()

docker/auth.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,12 @@ def resolve_authconfig(authconfig, registry=None):
9090
log.debug(
9191
'Using credentials store "{0}"'.format(store_name)
9292
)
93-
return _resolve_authconfig_credstore(
93+
cfg = _resolve_authconfig_credstore(
9494
authconfig, registry, store_name
9595
)
96+
if cfg is not None:
97+
return cfg
98+
log.debug('No entry in credstore - fetching from auth dict')
9699

97100
# Default to the public index server
98101
registry = resolve_index_name(registry) if registry else INDEX_NAME

docker/constants.py

+1
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@
1717

1818
DEFAULT_USER_AGENT = "docker-sdk-python/{0}".format(version)
1919
DEFAULT_NUM_POOLS = 25
20+
DEFAULT_DATA_CHUNK_SIZE = 1024 * 2048

docker/models/containers.py

+16-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from collections import namedtuple
44

55
from ..api import APIClient
6+
from ..constants import DEFAULT_DATA_CHUNK_SIZE
67
from ..errors import (ContainerError, ImageNotFound,
78
create_unexpected_kwargs_error)
89
from ..types import HostConfig
@@ -181,26 +182,34 @@ def exec_run(self, cmd, stdout=True, stderr=True, stdin=False, tty=False,
181182
exec_output
182183
)
183184

184-
def export(self):
185+
def export(self, chunk_size=DEFAULT_DATA_CHUNK_SIZE):
185186
"""
186187
Export the contents of the container's filesystem as a tar archive.
187188
189+
Args:
190+
chunk_size (int): The number of bytes returned by each iteration
191+
of the generator. If ``None``, data will be streamed as it is
192+
received. Default: 2 MB
193+
188194
Returns:
189195
(str): The filesystem tar archive
190196
191197
Raises:
192198
:py:class:`docker.errors.APIError`
193199
If the server returns an error.
194200
"""
195-
return self.client.api.export(self.id)
201+
return self.client.api.export(self.id, chunk_size)
196202

197-
def get_archive(self, path):
203+
def get_archive(self, path, chunk_size=DEFAULT_DATA_CHUNK_SIZE):
198204
"""
199205
Retrieve a file or folder from the container in the form of a tar
200206
archive.
201207
202208
Args:
203209
path (str): Path to the file or folder to retrieve
210+
chunk_size (int): The number of bytes returned by each iteration
211+
of the generator. If ``None``, data will be streamed as it is
212+
received. Default: 2 MB
204213
205214
Returns:
206215
(tuple): First element is a raw tar data stream. Second element is
@@ -210,7 +219,7 @@ def get_archive(self, path):
210219
:py:class:`docker.errors.APIError`
211220
If the server returns an error.
212221
"""
213-
return self.client.api.get_archive(self.id, path)
222+
return self.client.api.get_archive(self.id, path, chunk_size)
214223

215224
def kill(self, signal=None):
216225
"""
@@ -515,6 +524,8 @@ def run(self, image, command=None, stdout=True, stderr=False,
515524
(``0-3``, ``0,1``). Only effective on NUMA systems.
516525
detach (bool): Run container in the background and return a
517526
:py:class:`Container` object.
527+
device_cgroup_rules (:py:class:`list`): A list of cgroup rules to
528+
apply to the container.
518529
device_read_bps: Limit read rate (bytes per second) from a device
519530
in the form of: `[{"Path": "device_path", "Rate": rate}]`
520531
device_read_iops: Limit read rate (IO per second) from a device.
@@ -912,6 +923,7 @@ def prune(self, filters=None):
912923
'cpuset_mems',
913924
'cpu_rt_period',
914925
'cpu_rt_runtime',
926+
'device_cgroup_rules',
915927
'device_read_bps',
916928
'device_read_iops',
917929
'device_write_bps',

docker/models/images.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import six
55

66
from ..api import APIClient
7+
from ..constants import DEFAULT_DATA_CHUNK_SIZE
78
from ..errors import BuildError, ImageLoadError
89
from ..utils import parse_repository_tag
910
from ..utils.json_stream import json_stream
@@ -58,10 +59,15 @@ def history(self):
5859
"""
5960
return self.client.api.history(self.id)
6061

61-
def save(self):
62+
def save(self, chunk_size=DEFAULT_DATA_CHUNK_SIZE):
6263
"""
6364
Get a tarball of an image. Similar to the ``docker save`` command.
6465
66+
Args:
67+
chunk_size (int): The number of bytes returned by each iteration
68+
of the generator. If ``None``, data will be streamed as it is
69+
received. Default: 2 MB
70+
6571
Returns:
6672
(generator): A stream of raw archive data.
6773
@@ -77,7 +83,7 @@ def save(self):
7783
>>> f.write(chunk)
7884
>>> f.close()
7985
"""
80-
return self.client.api.get_image(self.id)
86+
return self.client.api.get_image(self.id, chunk_size)
8187

8288
def tag(self, repository, tag=None, **kwargs):
8389
"""
@@ -308,7 +314,9 @@ def pull(self, repository, tag=None, **kwargs):
308314

309315
self.client.api.pull(repository, tag=tag, **kwargs)
310316
if tag:
311-
return self.get('{0}:{1}'.format(repository, tag))
317+
return self.get('{0}{2}{1}'.format(
318+
repository, tag, '@' if tag.startswith('sha256:') else ':'
319+
))
312320
return self.list(repository)
313321

314322
def push(self, repository, tag=None, **kwargs):

docker/models/services.py

+15
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ def update(self, **kwargs):
6969
spec = self.attrs['Spec']['TaskTemplate']['ContainerSpec']
7070
kwargs['image'] = spec['Image']
7171

72+
if kwargs.get('force_update') is True:
73+
task_template = self.attrs['Spec']['TaskTemplate']
74+
current_value = int(task_template.get('ForceUpdate', 0))
75+
kwargs['force_update'] = current_value + 1
76+
7277
create_kwargs = _get_create_service_kwargs('update', kwargs)
7378

7479
return self.client.api.update_service(
@@ -124,6 +129,16 @@ def scale(self, replicas):
124129
service_mode,
125130
fetch_current_spec=True)
126131

132+
def force_update(self):
133+
"""
134+
Force update the service even if no changes require it.
135+
136+
Returns:
137+
``True``if successful.
138+
"""
139+
140+
return self.update(force_update=True, fetch_current_spec=True)
141+
127142

128143
class ServiceCollection(Collection):
129144
"""Services on the Docker server."""

docker/types/containers.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ def __init__(self, version, binds=None, port_bindings=None,
120120
init=None, init_path=None, volume_driver=None,
121121
cpu_count=None, cpu_percent=None, nano_cpus=None,
122122
cpuset_mems=None, runtime=None, mounts=None,
123-
cpu_rt_period=None, cpu_rt_runtime=None):
123+
cpu_rt_period=None, cpu_rt_runtime=None,
124+
device_cgroup_rules=None):
124125

125126
if mem_limit is not None:
126127
self['Memory'] = parse_bytes(mem_limit)
@@ -466,6 +467,15 @@ def __init__(self, version, binds=None, port_bindings=None,
466467
raise host_config_version_error('mounts', '1.30')
467468
self['Mounts'] = mounts
468469

470+
if device_cgroup_rules is not None:
471+
if version_lt(version, '1.28'):
472+
raise host_config_version_error('device_cgroup_rules', '1.28')
473+
if not isinstance(device_cgroup_rules, list):
474+
raise host_config_type_error(
475+
'device_cgroup_rules', device_cgroup_rules, 'list'
476+
)
477+
self['DeviceCgroupRules'] = device_cgroup_rules
478+
469479

470480
def host_config_type_error(param, param_value, expected):
471481
error_msg = 'Invalid type for {0} param: expected {1} but found {2}'

docker/types/services.py

+35-2
Original file line numberDiff line numberDiff line change
@@ -306,9 +306,13 @@ class Resources(dict):
306306
mem_limit (int): Memory limit in Bytes.
307307
cpu_reservation (int): CPU reservation in units of 10^9 CPU shares.
308308
mem_reservation (int): Memory reservation in Bytes.
309+
generic_resources (dict or :py:class:`list`): Node level generic
310+
resources, for example a GPU, using the following format:
311+
``{ resource_name: resource_value }``. Alternatively, a list of
312+
of resource specifications as defined by the Engine API.
309313
"""
310314
def __init__(self, cpu_limit=None, mem_limit=None, cpu_reservation=None,
311-
mem_reservation=None):
315+
mem_reservation=None, generic_resources=None):
312316
limits = {}
313317
reservation = {}
314318
if cpu_limit is not None:
@@ -319,13 +323,42 @@ def __init__(self, cpu_limit=None, mem_limit=None, cpu_reservation=None,
319323
reservation['NanoCPUs'] = cpu_reservation
320324
if mem_reservation is not None:
321325
reservation['MemoryBytes'] = mem_reservation
322-
326+
if generic_resources is not None:
327+
reservation['GenericResources'] = (
328+
_convert_generic_resources_dict(generic_resources)
329+
)
323330
if limits:
324331
self['Limits'] = limits
325332
if reservation:
326333
self['Reservations'] = reservation
327334

328335

336+
def _convert_generic_resources_dict(generic_resources):
337+
if isinstance(generic_resources, list):
338+
return generic_resources
339+
if not isinstance(generic_resources, dict):
340+
raise errors.InvalidArgument(
341+
'generic_resources must be a dict or a list'
342+
' (found {})'.format(type(generic_resources))
343+
)
344+
resources = []
345+
for kind, value in six.iteritems(generic_resources):
346+
resource_type = None
347+
if isinstance(value, int):
348+
resource_type = 'DiscreteResourceSpec'
349+
elif isinstance(value, str):
350+
resource_type = 'NamedResourceSpec'
351+
else:
352+
raise errors.InvalidArgument(
353+
'Unsupported generic resource reservation '
354+
'type: {}'.format({kind: value})
355+
)
356+
resources.append({
357+
resource_type: {'Kind': kind, 'Value': value}
358+
})
359+
return resources
360+
361+
329362
class UpdateConfig(dict):
330363
"""
331364

0 commit comments

Comments
 (0)