diff options
author | Joffrey F <joffrey@docker.com> | 2016-08-19 16:46:21 -0700 |
---|---|---|
committer | Joffrey F <joffrey@docker.com> | 2016-08-19 16:51:13 -0700 |
commit | 97094e4ea303b59ce05869132cf639305a6d947a (patch) | |
tree | 2d07d95e8df436a6c642d0a195eafc16bd04bf6a | |
parent | 02e99e4967bebb116a7d9d650647df912c608297 (diff) | |
download | docker-py-97094e4ea303b59ce05869132cf639305a6d947a.tar.gz |
New docker.types subpackage containing advanced config dictionary types
Tests and docs updated to match
docker.utils.types has been moved to docker.types
Signed-off-by: Joffrey F <joffrey@docker.com>
-rw-r--r-- | docker/api/__init__.py | 5 | ||||
-rw-r--r-- | docker/api/service.py | 187 | ||||
-rw-r--r-- | docker/types/__init__.py | 7 | ||||
-rw-r--r-- | docker/types/base.py | 7 | ||||
-rw-r--r-- | docker/types/containers.py (renamed from docker/utils/types.py) | 50 | ||||
-rw-r--r-- | docker/types/services.py | 176 | ||||
-rw-r--r-- | docker/types/swarm.py | 40 | ||||
-rw-r--r-- | docker/utils/__init__.py | 6 | ||||
-rw-r--r-- | docker/utils/utils.py | 2 | ||||
-rw-r--r-- | docs/swarm.md | 8 | ||||
-rw-r--r-- | setup.py | 3 | ||||
-rw-r--r-- | tests/integration/service_test.py | 96 |
12 files changed, 345 insertions, 242 deletions
diff --git a/docker/api/__init__.py b/docker/api/__init__.py index 3c74677..bc7e93c 100644 --- a/docker/api/__init__.py +++ b/docker/api/__init__.py @@ -5,9 +5,6 @@ from .daemon import DaemonApiMixin from .exec_api import ExecApiMixin from .image import ImageApiMixin from .network import NetworkApiMixin -from .service import ( - ServiceApiMixin, TaskTemplate, ContainerSpec, Mount, Resources, - RestartPolicy, UpdateConfig -) +from .service import ServiceApiMixin from .swarm import SwarmApiMixin from .volume import VolumeApiMixin diff --git a/docker/api/service.py b/docker/api/service.py index db19ae5..c62e494 100644 --- a/docker/api/service.py +++ b/docker/api/service.py @@ -1,28 +1,17 @@ -import six - -from .. import errors from .. import utils class ServiceApiMixin(object): @utils.minimum_version('1.24') - def services(self, filters=None): - params = { - 'filters': utils.convert_filters(filters) if filters else None - } - url = self._url('/services') - return self._result(self._get(url, params=params), True) - - @utils.minimum_version('1.24') def create_service( - self, task_config, name=None, labels=None, mode=None, + self, task_template, name=None, labels=None, mode=None, update_config=None, networks=None, endpoint_config=None ): url = self._url('/services/create') data = { 'Name': name, 'Labels': labels, - 'TaskTemplate': task_config, + 'TaskTemplate': task_template, 'Mode': mode, 'UpdateConfig': update_config, 'Networks': networks, @@ -45,6 +34,14 @@ class ServiceApiMixin(object): return True @utils.minimum_version('1.24') + def services(self, filters=None): + params = { + 'filters': utils.convert_filters(filters) if filters else None + } + url = self._url('/services') + return self._result(self._get(url, params=params), True) + + @utils.minimum_version('1.24') @utils.check_resource def update_service(self, service, version, task_template=None, name=None, labels=None, mode=None, update_config=None, @@ -69,167 +66,3 @@ class ServiceApiMixin(object): resp = self._post_json(url, data=data, params={'version': version}) self._raise_for_status(resp) return True - - -class TaskTemplate(dict): - def __init__(self, container_spec, resources=None, restart_policy=None, - placement=None, log_driver=None): - self['ContainerSpec'] = container_spec - if resources: - self['Resources'] = resources - if restart_policy: - self['RestartPolicy'] = restart_policy - if placement: - self['Placement'] = placement - if log_driver: - self['LogDriver'] = log_driver - - @property - def container_spec(self): - return self.get('ContainerSpec') - - @property - def resources(self): - return self.get('Resources') - - @property - def restart_policy(self): - return self.get('RestartPolicy') - - @property - def placement(self): - return self.get('Placement') - - -class ContainerSpec(dict): - def __init__(self, image, command=None, args=None, env=None, workdir=None, - user=None, labels=None, mounts=None, stop_grace_period=None): - self['Image'] = image - self['Command'] = command - self['Args'] = args - - if env is not None: - self['Env'] = env - if workdir is not None: - self['Dir'] = workdir - if user is not None: - self['User'] = user - if labels is not None: - self['Labels'] = labels - if mounts is not None: - for mount in mounts: - if isinstance(mount, six.string_types): - mounts.append(Mount.parse_mount_string(mount)) - mounts.remove(mount) - self['Mounts'] = mounts - if stop_grace_period is not None: - self['StopGracePeriod'] = stop_grace_period - - -class Mount(dict): - def __init__(self, target, source, type='volume', read_only=False, - propagation=None, no_copy=False, labels=None, - driver_config=None): - self['Target'] = target - self['Source'] = source - if type not in ('bind', 'volume'): - raise errors.DockerError( - 'Only acceptable mount types are `bind` and `volume`.' - ) - self['Type'] = type - - if type == 'bind': - if propagation is not None: - self['BindOptions'] = { - 'Propagation': propagation - } - if any(labels, driver_config, no_copy): - raise errors.DockerError( - 'Mount type is binding but volume options have been ' - 'provided.' - ) - else: - volume_opts = {} - if no_copy: - volume_opts['NoCopy'] = True - if labels: - volume_opts['Labels'] = labels - if driver_config: - volume_opts['driver_config'] = driver_config - if volume_opts: - self['VolumeOptions'] = volume_opts - if propagation: - raise errors.DockerError( - 'Mount type is volume but `propagation` argument has been ' - 'provided.' - ) - - @classmethod - def parse_mount_string(cls, string): - parts = string.split(':') - if len(parts) > 3: - raise errors.DockerError( - 'Invalid mount format "{0}"'.format(string) - ) - if len(parts) == 1: - return cls(target=parts[0]) - else: - target = parts[1] - source = parts[0] - read_only = not (len(parts) == 3 or parts[2] == 'ro') - return cls(target, source, read_only=read_only) - - -class Resources(dict): - def __init__(self, cpu_limit=None, mem_limit=None, cpu_reservation=None, - mem_reservation=None): - limits = {} - reservation = {} - if cpu_limit is not None: - limits['NanoCPUs'] = cpu_limit - if mem_limit is not None: - limits['MemoryBytes'] = mem_limit - if cpu_reservation is not None: - reservation['NanoCPUs'] = cpu_reservation - if mem_reservation is not None: - reservation['MemoryBytes'] = mem_reservation - - self['Limits'] = limits - self['Reservation'] = reservation - - -class UpdateConfig(dict): - def __init__(self, parallelism=0, delay=None, failure_action='continue'): - self['Parallelism'] = parallelism - if delay is not None: - self['Delay'] = delay - if failure_action not in ('pause', 'continue'): - raise errors.DockerError( - 'failure_action must be either `pause` or `continue`.' - ) - self['FailureAction'] = failure_action - - -class RestartConditionTypesEnum(object): - _values = ( - 'none', - 'on_failure', - 'any', - ) - NONE, ON_FAILURE, ANY = _values - - -class RestartPolicy(dict): - condition_types = RestartConditionTypesEnum - - def __init__(self, condition=RestartConditionTypesEnum.NONE, delay=0, - attempts=0, window=0): - if condition not in self.condition_types._values: - raise TypeError( - 'Invalid RestartPolicy condition {0}'.format(condition) - ) - - self['Condition'] = condition - self['Delay'] = delay - self['Attempts'] = attempts - self['Window'] = window diff --git a/docker/types/__init__.py b/docker/types/__init__.py new file mode 100644 index 0000000..46f10d8 --- /dev/null +++ b/docker/types/__init__.py @@ -0,0 +1,7 @@ +# flake8: noqa +from .containers import LogConfig, Ulimit +from .services import ( + ContainerSpec, LogDriver, Mount, Resources, RestartPolicy, TaskTemplate, + UpdateConfig +) +from .swarm import SwarmSpec, SwarmExternalCA diff --git a/docker/types/base.py b/docker/types/base.py new file mode 100644 index 0000000..6891062 --- /dev/null +++ b/docker/types/base.py @@ -0,0 +1,7 @@ +import six + + +class DictType(dict): + def __init__(self, init): + for k, v in six.iteritems(init): + self[k] = v diff --git a/docker/utils/types.py b/docker/types/containers.py index d778b90..40a44ca 100644 --- a/docker/utils/types.py +++ b/docker/types/containers.py @@ -1,5 +1,7 @@ import six +from .base import DictType + class LogConfigTypesEnum(object): _values = ( @@ -13,12 +15,6 @@ class LogConfigTypesEnum(object): JSON, SYSLOG, JOURNALD, GELF, FLUENTD, NONE = _values -class DictType(dict): - def __init__(self, init): - for k, v in six.iteritems(init): - self[k] = v - - class LogConfig(DictType): types = LogConfigTypesEnum @@ -94,45 +90,3 @@ class Ulimit(DictType): @hard.setter def hard(self, value): self['Hard'] = value - - -class SwarmSpec(DictType): - def __init__(self, task_history_retention_limit=None, - snapshot_interval=None, keep_old_snapshots=None, - log_entries_for_slow_followers=None, heartbeat_tick=None, - election_tick=None, dispatcher_heartbeat_period=None, - node_cert_expiry=None, external_ca=None, name=None): - if task_history_retention_limit is not None: - self['Orchestration'] = { - 'TaskHistoryRetentionLimit': task_history_retention_limit - } - if any([snapshot_interval, keep_old_snapshots, - log_entries_for_slow_followers, heartbeat_tick, election_tick]): - self['Raft'] = { - 'SnapshotInterval': snapshot_interval, - 'KeepOldSnapshots': keep_old_snapshots, - 'LogEntriesForSlowFollowers': log_entries_for_slow_followers, - 'HeartbeatTick': heartbeat_tick, - 'ElectionTick': election_tick - } - - if dispatcher_heartbeat_period: - self['Dispatcher'] = { - 'HeartbeatPeriod': dispatcher_heartbeat_period - } - - if node_cert_expiry or external_ca: - self['CAConfig'] = { - 'NodeCertExpiry': node_cert_expiry, - 'ExternalCA': external_ca - } - - if name is not None: - self['Name'] = name - - -class SwarmExternalCA(DictType): - def __init__(self, url, protocol=None, options=None): - self['URL'] = url - self['Protocol'] = protocol - self['Options'] = options diff --git a/docker/types/services.py b/docker/types/services.py new file mode 100644 index 0000000..6a17e93 --- /dev/null +++ b/docker/types/services.py @@ -0,0 +1,176 @@ +import six + +from .. import errors + + +class TaskTemplate(dict): + def __init__(self, container_spec, resources=None, restart_policy=None, + placement=None, log_driver=None): + self['ContainerSpec'] = container_spec + if resources: + self['Resources'] = resources + if restart_policy: + self['RestartPolicy'] = restart_policy + if placement: + self['Placement'] = placement + if log_driver: + self['LogDriver'] = log_driver + + @property + def container_spec(self): + return self.get('ContainerSpec') + + @property + def resources(self): + return self.get('Resources') + + @property + def restart_policy(self): + return self.get('RestartPolicy') + + @property + def placement(self): + return self.get('Placement') + + +class ContainerSpec(dict): + def __init__(self, image, command=None, args=None, env=None, workdir=None, + user=None, labels=None, mounts=None, stop_grace_period=None): + self['Image'] = image + self['Command'] = command + self['Args'] = args + + if env is not None: + self['Env'] = env + if workdir is not None: + self['Dir'] = workdir + if user is not None: + self['User'] = user + if labels is not None: + self['Labels'] = labels + if mounts is not None: + for mount in mounts: + if isinstance(mount, six.string_types): + mounts.append(Mount.parse_mount_string(mount)) + mounts.remove(mount) + self['Mounts'] = mounts + if stop_grace_period is not None: + self['StopGracePeriod'] = stop_grace_period + + +class Mount(dict): + def __init__(self, target, source, type='volume', read_only=False, + propagation=None, no_copy=False, labels=None, + driver_config=None): + self['Target'] = target + self['Source'] = source + if type not in ('bind', 'volume'): + raise errors.DockerError( + 'Only acceptable mount types are `bind` and `volume`.' + ) + self['Type'] = type + + if type == 'bind': + if propagation is not None: + self['BindOptions'] = { + 'Propagation': propagation + } + if any(labels, driver_config, no_copy): + raise errors.DockerError( + 'Mount type is binding but volume options have been ' + 'provided.' + ) + else: + volume_opts = {} + if no_copy: + volume_opts['NoCopy'] = True + if labels: + volume_opts['Labels'] = labels + if driver_config: + volume_opts['driver_config'] = driver_config + if volume_opts: + self['VolumeOptions'] = volume_opts + if propagation: + raise errors.DockerError( + 'Mount type is volume but `propagation` argument has been ' + 'provided.' + ) + + @classmethod + def parse_mount_string(cls, string): + parts = string.split(':') + if len(parts) > 3: + raise errors.DockerError( + 'Invalid mount format "{0}"'.format(string) + ) + if len(parts) == 1: + return cls(target=parts[0]) + else: + target = parts[1] + source = parts[0] + read_only = not (len(parts) == 3 or parts[2] == 'ro') + return cls(target, source, read_only=read_only) + + +class Resources(dict): + def __init__(self, cpu_limit=None, mem_limit=None, cpu_reservation=None, + mem_reservation=None): + limits = {} + reservation = {} + if cpu_limit is not None: + limits['NanoCPUs'] = cpu_limit + if mem_limit is not None: + limits['MemoryBytes'] = mem_limit + if cpu_reservation is not None: + reservation['NanoCPUs'] = cpu_reservation + if mem_reservation is not None: + reservation['MemoryBytes'] = mem_reservation + + if limits: + self['Limits'] = limits + if reservation: + self['Reservations'] = reservation + + +class UpdateConfig(dict): + def __init__(self, parallelism=0, delay=None, failure_action='continue'): + self['Parallelism'] = parallelism + if delay is not None: + self['Delay'] = delay + if failure_action not in ('pause', 'continue'): + raise errors.DockerError( + 'failure_action must be either `pause` or `continue`.' + ) + self['FailureAction'] = failure_action + + +class RestartConditionTypesEnum(object): + _values = ( + 'none', + 'on_failure', + 'any', + ) + NONE, ON_FAILURE, ANY = _values + + +class RestartPolicy(dict): + condition_types = RestartConditionTypesEnum + + def __init__(self, condition=RestartConditionTypesEnum.NONE, delay=0, + max_attempts=0, window=0): + if condition not in self.condition_types._values: + raise TypeError( + 'Invalid RestartPolicy condition {0}'.format(condition) + ) + + self['Condition'] = condition + self['Delay'] = delay + self['MaxAttempts'] = max_attempts + self['Window'] = window + + +class LogDriver(dict): + def __init__(self, name, options=None): + self['Name'] = name + if options: + self['Options'] = options diff --git a/docker/types/swarm.py b/docker/types/swarm.py new file mode 100644 index 0000000..865fde6 --- /dev/null +++ b/docker/types/swarm.py @@ -0,0 +1,40 @@ +class SwarmSpec(dict): + def __init__(self, task_history_retention_limit=None, + snapshot_interval=None, keep_old_snapshots=None, + log_entries_for_slow_followers=None, heartbeat_tick=None, + election_tick=None, dispatcher_heartbeat_period=None, + node_cert_expiry=None, external_ca=None, name=None): + if task_history_retention_limit is not None: + self['Orchestration'] = { + 'TaskHistoryRetentionLimit': task_history_retention_limit + } + if any([snapshot_interval, keep_old_snapshots, + log_entries_for_slow_followers, heartbeat_tick, election_tick]): + self['Raft'] = { + 'SnapshotInterval': snapshot_interval, + 'KeepOldSnapshots': keep_old_snapshots, + 'LogEntriesForSlowFollowers': log_entries_for_slow_followers, + 'HeartbeatTick': heartbeat_tick, + 'ElectionTick': election_tick + } + + if dispatcher_heartbeat_period: + self['Dispatcher'] = { + 'HeartbeatPeriod': dispatcher_heartbeat_period + } + + if node_cert_expiry or external_ca: + self['CAConfig'] = { + 'NodeCertExpiry': node_cert_expiry, + 'ExternalCA': external_ca + } + + if name is not None: + self['Name'] = name + + +class SwarmExternalCA(dict): + def __init__(self, url, protocol=None, options=None): + self['URL'] = url + self['Protocol'] = protocol + self['Options'] = options diff --git a/docker/utils/__init__.py b/docker/utils/__init__.py index 35acc77..4bb3876 100644 --- a/docker/utils/__init__.py +++ b/docker/utils/__init__.py @@ -8,8 +8,6 @@ from .utils import ( create_ipam_config, create_ipam_pool, parse_devices, normalize_links, ) -from .types import LogConfig, Ulimit -from .types import ( - SwarmExternalCA, SwarmSpec, -) +from ..types import LogConfig, Ulimit +from ..types import SwarmExternalCA, SwarmSpec from .decorators import check_resource, minimum_version, update_headers diff --git a/docker/utils/utils.py b/docker/utils/utils.py index 00a7af1..bea436a 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -31,7 +31,7 @@ import six from .. import constants from .. import errors from .. import tls -from .types import Ulimit, LogConfig +from ..types import Ulimit, LogConfig if six.PY2: from urllib import splitnport diff --git a/docs/swarm.md b/docs/swarm.md index 0cd015a..3cc44f8 100644 --- a/docs/swarm.md +++ b/docs/swarm.md @@ -95,7 +95,7 @@ Initialize a new Swarm using the current connected engine as the first node. #### Client.create_swarm_spec -Create a `docker.utils.SwarmSpec` instance that can be used as the `swarm_spec` +Create a `docker.types.SwarmSpec` instance that can be used as the `swarm_spec` argument in `Client.init_swarm`. **Params:** @@ -113,12 +113,12 @@ argument in `Client.init_swarm`. heartbeat to the dispatcher. * node_cert_expiry (int): Automatic expiry for nodes certificates. * external_ca (dict): Configuration for forwarding signing requests to an - external certificate authority. Use `docker.utils.SwarmExternalCA`. + external certificate authority. Use `docker.types.SwarmExternalCA`. * name (string): Swarm's name -**Returns:** `docker.utils.SwarmSpec` instance. +**Returns:** `docker.types.SwarmSpec` instance. -#### docker.utils.SwarmExternalCA +#### docker.types.SwarmExternalCA Create a configuration dictionary for the `external_ca` argument in a `SwarmSpec`. @@ -36,7 +36,8 @@ setup( url='https://github.com/docker/docker-py/', packages=[ 'docker', 'docker.api', 'docker.auth', 'docker.transport', - 'docker.utils', 'docker.utils.ports', 'docker.ssladapter' + 'docker.utils', 'docker.utils.ports', 'docker.ssladapter', + 'docker.types', ], install_requires=requirements, tests_require=test_requirements, diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 0010986..fda62b3 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -40,8 +40,10 @@ class ServiceTest(helpers.BaseTestCase): else: name = self.get_service_name() - container_spec = docker.api.ContainerSpec('busybox', ['echo', 'hello']) - task_tmpl = docker.api.TaskTemplate(container_spec) + container_spec = docker.types.ContainerSpec( + 'busybox', ['echo', 'hello'] + ) + task_tmpl = docker.types.TaskTemplate(container_spec) return name, self.client.create_service(task_tmpl, name=name) @requires_api_version('1.24') @@ -74,7 +76,7 @@ class ServiceTest(helpers.BaseTestCase): test_services = self.client.services(filters={'name': 'dockerpytest_'}) assert len(test_services) == 0 - def test_rempve_service_by_name(self): + def test_remove_service_by_name(self): svc_name, svc_id = self.create_simple_service() assert self.client.remove_service(svc_name) test_services = self.client.services(filters={'name': 'dockerpytest_'}) @@ -87,6 +89,94 @@ class ServiceTest(helpers.BaseTestCase): assert len(services) == 1 assert services[0]['ID'] == svc_id['ID'] + def test_create_service_custom_log_driver(self): + container_spec = docker.types.ContainerSpec( + 'busybox', ['echo', 'hello'] + ) + log_cfg = docker.types.LogDriver('none') + task_tmpl = docker.types.TaskTemplate( + container_spec, log_driver=log_cfg + ) + name = self.get_service_name() + svc_id = self.client.create_service(task_tmpl, name=name) + svc_info = self.client.inspect_service(svc_id) + assert 'TaskTemplate' in svc_info['Spec'] + res_template = svc_info['Spec']['TaskTemplate'] + assert 'LogDriver' in res_template + assert 'Name' in res_template['LogDriver'] + assert res_template['LogDriver']['Name'] == 'none' + + def test_create_service_with_volume_mount(self): + vol_name = self.get_service_name() + container_spec = docker.types.ContainerSpec( + 'busybox', ['ls'], + mounts=[ + docker.types.Mount(target='/test', source=vol_name) + ] + ) + self.tmp_volumes.append(vol_name) + task_tmpl = docker.types.TaskTemplate(container_spec) + name = self.get_service_name() + svc_id = self.client.create_service(task_tmpl, name=name) + svc_info = self.client.inspect_service(svc_id) + assert 'ContainerSpec' in svc_info['Spec']['TaskTemplate'] + cspec = svc_info['Spec']['TaskTemplate']['ContainerSpec'] + assert 'Mounts' in cspec + assert len(cspec['Mounts']) == 1 + mount = cspec['Mounts'][0] + assert mount['Target'] == '/test' + assert mount['Source'] == vol_name + assert mount['Type'] == 'volume' + + def test_create_service_with_resources_constraints(self): + container_spec = docker.types.ContainerSpec('busybox', ['true']) + resources = docker.types.Resources( + cpu_limit=4000000, mem_limit=3 * 1024 * 1024 * 1024, + cpu_reservation=3500000, mem_reservation=2 * 1024 * 1024 * 1024 + ) + task_tmpl = docker.types.TaskTemplate( + container_spec, resources=resources + ) + name = self.get_service_name() + svc_id = self.client.create_service(task_tmpl, name=name) + svc_info = self.client.inspect_service(svc_id) + assert 'TaskTemplate' in svc_info['Spec'] + res_template = svc_info['Spec']['TaskTemplate'] + assert 'Resources' in res_template + assert res_template['Resources']['Limits'] == resources['Limits'] + assert res_template['Resources']['Reservations'] == resources[ + 'Reservations' + ] + + def test_create_service_with_update_config(self): + container_spec = docker.types.ContainerSpec('busybox', ['true']) + task_tmpl = docker.types.TaskTemplate(container_spec) + update_config = docker.types.UpdateConfig( + parallelism=10, delay=5, failure_action='pause' + ) + name = self.get_service_name() + svc_id = self.client.create_service( + task_tmpl, update_config=update_config, name=name + ) + svc_info = self.client.inspect_service(svc_id) + assert 'UpdateConfig' in svc_info['Spec'] + assert update_config == svc_info['Spec']['UpdateConfig'] + + def test_create_service_with_restart_policy(self): + container_spec = docker.types.ContainerSpec('busybox', ['true']) + policy = docker.types.RestartPolicy( + docker.types.RestartPolicy.condition_types.ANY, + delay=5, max_attempts=5 + ) + task_tmpl = docker.types.TaskTemplate( + container_spec, restart_policy=policy + ) + name = self.get_service_name() + svc_id = self.client.create_service(task_tmpl, name=name) + svc_info = self.client.inspect_service(svc_id) + assert 'RestartPolicy' in svc_info['Spec']['TaskTemplate'] + assert policy == svc_info['Spec']['TaskTemplate']['RestartPolicy'] + def test_update_service_name(self): name, svc_id = self.create_simple_service() svc_info = self.client.inspect_service(svc_id) |