summaryrefslogtreecommitdiff
path: root/docker/models/services.py
blob: 458d2c873051fc0ce81ee9cf6010dafbae3b85af (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
import copy
from docker.errors import create_unexpected_kwargs_error, InvalidArgument
from docker.types import TaskTemplate, ContainerSpec, ServiceMode
from .resource import Model, Collection


class Service(Model):
    """A service."""
    id_attribute = 'ID'

    @property
    def name(self):
        """The service's name."""
        return self.attrs['Spec']['Name']

    @property
    def version(self):
        """
        The version number of the service. If this is not the same as the
        server, the :py:meth:`update` function will not work and you will
        need to call :py:meth:`reload` before calling it again.
        """
        return self.attrs.get('Version').get('Index')

    def remove(self):
        """
        Stop and remove the service.

        Raises:
            :py:class:`docker.errors.APIError`
                If the server returns an error.
        """
        return self.client.api.remove_service(self.id)

    def tasks(self, filters=None):
        """
        List the tasks in this service.

        Args:
            filters (dict): A map of filters to process on the tasks list.
                Valid filters: ``id``, ``name``, ``node``,
                ``label``, and ``desired-state``.

        Returns:
            (:py:class:`list`): List of task dictionaries.

        Raises:
            :py:class:`docker.errors.APIError`
                If the server returns an error.
        """
        if filters is None:
            filters = {}
        filters['service'] = self.id
        return self.client.api.tasks(filters=filters)

    def update(self, **kwargs):
        """
        Update a service's configuration. Similar to the ``docker service
        update`` command.

        Takes the same parameters as :py:meth:`~ServiceCollection.create`.

        Raises:
            :py:class:`docker.errors.APIError`
                If the server returns an error.
        """
        # Image is required, so if it hasn't been set, use current image
        if 'image' not in kwargs:
            spec = self.attrs['Spec']['TaskTemplate']['ContainerSpec']
            kwargs['image'] = spec['Image']

        if kwargs.get('force_update') is True:
            task_template = self.attrs['Spec']['TaskTemplate']
            current_value = int(task_template.get('ForceUpdate', 0))
            kwargs['force_update'] = current_value + 1

        create_kwargs = _get_create_service_kwargs('update', kwargs)

        return self.client.api.update_service(
            self.id,
            self.version,
            **create_kwargs
        )

    def logs(self, **kwargs):
        """
            Get log stream for the service.
            Note: This method works only for services with the ``json-file``
            or ``journald`` logging drivers.

            Args:
                details (bool): Show extra details provided to logs.
                    Default: ``False``
                follow (bool): Keep connection open to read logs as they are
                    sent by the Engine. Default: ``False``
                stdout (bool): Return logs from ``stdout``. Default: ``False``
                stderr (bool): Return logs from ``stderr``. Default: ``False``
                since (int): UNIX timestamp for the logs staring point.
                    Default: 0
                timestamps (bool): Add timestamps to every log line.
                tail (string or int): Number of log lines to be returned,
                    counting from the current end of the logs. Specify an
                    integer or ``'all'`` to output all log lines.
                    Default: ``all``

            Returns (generator): Logs for the service.
        """
        is_tty = self.attrs['Spec']['TaskTemplate']['ContainerSpec'].get(
            'TTY', False
        )
        return self.client.api.service_logs(self.id, is_tty=is_tty, **kwargs)

    def scale(self, replicas):
        """
        Scale service container.

        Args:
            replicas (int): The number of containers that should be running.

        Returns:
            ``True``if successful.
        """

        if 'Global' in self.attrs['Spec']['Mode'].keys():
            raise InvalidArgument('Cannot scale a global container')

        service_mode = ServiceMode('replicated', replicas)
        return self.client.api.update_service(self.id, self.version,
                                              mode=service_mode,
                                              fetch_current_spec=True)

    def force_update(self):
        """
        Force update the service even if no changes require it.

        Returns:
            ``True``if successful.
        """

        return self.update(force_update=True, fetch_current_spec=True)


class ServiceCollection(Collection):
    """Services on the Docker server."""
    model = Service

    def create(self, image, command=None, **kwargs):
        """
        Create a service. Similar to the ``docker service create`` command.

        Args:
            image (str): The image name to use for the containers.
            command (list of str or str): Command to run.
            args (list of str): Arguments to the command.
            constraints (list of str): Placement constraints.
            container_labels (dict): Labels to apply to the container.
            endpoint_spec (EndpointSpec): Properties that can be configured to
                access and load balance a service. Default: ``None``.
            env (list of str): Environment variables, in the form
                ``KEY=val``.
            hostname (string): Hostname to set on the container.
            isolation (string): Isolation technology used by the service's
                containers. Only used for Windows containers.
            labels (dict): Labels to apply to the service.
            log_driver (str): Log driver to use for containers.
            log_driver_options (dict): Log driver options.
            mode (ServiceMode): Scheduling mode for the service.
                Default:``None``
            mounts (list of str): Mounts for the containers, in the form
                ``source:target:options``, where options is either
                ``ro`` or ``rw``.
            name (str): Name to give to the service.
            networks (list of str): List of network names or IDs to attach
                the service to. Default: ``None``.
            resources (Resources): Resource limits and reservations.
            restart_policy (RestartPolicy): Restart policy for containers.
            secrets (list of :py:class:`docker.types.SecretReference`): List
                of secrets accessible to containers for this service.
            stop_grace_period (int): Amount of time to wait for
                containers to terminate before forcefully killing them.
            update_config (UpdateConfig): Specification for the update strategy
                of the service. Default: ``None``
            user (str): User to run commands as.
            workdir (str): Working directory for commands to run.
            tty (boolean): Whether a pseudo-TTY should be allocated.
            groups (:py:class:`list`): A list of additional groups that the
                container process will run as.
            open_stdin (boolean): Open ``stdin``
            read_only (boolean): Mount the container's root filesystem as read
                only.
            stop_signal (string): Set signal to stop the service's containers
            healthcheck (Healthcheck): Healthcheck
                configuration for this service.
            hosts (:py:class:`dict`): A set of host to IP mappings to add to
                the container's `hosts` file.
            dns_config (DNSConfig): Specification for DNS
                related configurations in resolver configuration file.
            configs (:py:class:`list`): List of :py:class:`ConfigReference`
                that will be exposed to the service.
            privileges (Privileges): Security options for the service's
                containers.

        Returns:
            (:py:class:`Service`) The created service.

        Raises:
            :py:class:`docker.errors.APIError`
                If the server returns an error.
        """
        kwargs['image'] = image
        kwargs['command'] = command
        create_kwargs = _get_create_service_kwargs('create', kwargs)
        service_id = self.client.api.create_service(**create_kwargs)
        return self.get(service_id)

    def get(self, service_id, insert_defaults=None):
        """
        Get a service.

        Args:
            service_id (str): The ID of the service.
            insert_defaults (boolean): If true, default values will be merged
                into the output.

        Returns:
            (:py:class:`Service`): The service.

        Raises:
            :py:class:`docker.errors.NotFound`
                If the service does not exist.
            :py:class:`docker.errors.APIError`
                If the server returns an error.
            :py:class:`docker.errors.InvalidVersion`
                If one of the arguments is not supported with the current
                API version.
        """
        return self.prepare_model(
            self.client.api.inspect_service(service_id, insert_defaults)
        )

    def list(self, **kwargs):
        """
        List services.

        Args:
            filters (dict): Filters to process on the nodes list. Valid
                filters: ``id``, ``name`` , ``label`` and ``mode``.
                Default: ``None``.

        Returns:
            (list of :py:class:`Service`): The services.

        Raises:
            :py:class:`docker.errors.APIError`
                If the server returns an error.
        """
        return [
            self.prepare_model(s)
            for s in self.client.api.services(**kwargs)
        ]


# kwargs to copy straight over to ContainerSpec
CONTAINER_SPEC_KWARGS = [
    'args',
    'command',
    'configs',
    'dns_config',
    'env',
    'groups',
    'healthcheck',
    'hostname',
    'hosts',
    'image',
    'isolation',
    'labels',
    'mounts',
    'open_stdin',
    'privileges',
    'read_only',
    'secrets',
    'stop_grace_period',
    'stop_signal',
    'tty',
    'user',
    'workdir',
]

# kwargs to copy straight over to TaskTemplate
TASK_TEMPLATE_KWARGS = [
    'networks',
    'resources',
    'restart_policy',
]

# kwargs to copy straight over to create_service
CREATE_SERVICE_KWARGS = [
    'name',
    'labels',
    'mode',
    'update_config',
    'endpoint_spec',
]


def _get_create_service_kwargs(func_name, kwargs):
    # Copy over things which can be copied directly
    create_kwargs = {}
    for key in copy.copy(kwargs):
        if key in CREATE_SERVICE_KWARGS:
            create_kwargs[key] = kwargs.pop(key)
    container_spec_kwargs = {}
    for key in copy.copy(kwargs):
        if key in CONTAINER_SPEC_KWARGS:
            container_spec_kwargs[key] = kwargs.pop(key)
    task_template_kwargs = {}
    for key in copy.copy(kwargs):
        if key in TASK_TEMPLATE_KWARGS:
            task_template_kwargs[key] = kwargs.pop(key)

    if 'container_labels' in kwargs:
        container_spec_kwargs['labels'] = kwargs.pop('container_labels')

    if 'constraints' in kwargs:
        task_template_kwargs['placement'] = {
            'Constraints': kwargs.pop('constraints')
        }

    if 'log_driver' in kwargs:
        task_template_kwargs['log_driver'] = {
            'Name': kwargs.pop('log_driver'),
            'Options': kwargs.pop('log_driver_options', {})
        }

    if func_name == 'update':
        if 'force_update' in kwargs:
            task_template_kwargs['force_update'] = kwargs.pop('force_update')

        # fetch the current spec by default if updating the service
        # through the model
        fetch_current_spec = kwargs.pop('fetch_current_spec', True)
        create_kwargs['fetch_current_spec'] = fetch_current_spec

    # All kwargs should have been consumed by this point, so raise
    # error if any are left
    if kwargs:
        raise create_unexpected_kwargs_error(func_name, kwargs)

    container_spec = ContainerSpec(**container_spec_kwargs)
    task_template_kwargs['container_spec'] = container_spec
    create_kwargs['task_template'] = TaskTemplate(**task_template_kwargs)
    return create_kwargs