1from .. import auth, errors, utils
2from ..types import ServiceMode
3
4
5def _check_api_features(version, task_template, update_config, endpoint_spec,
6                        rollback_config):
7
8    def raise_version_error(param, min_version):
9        raise errors.InvalidVersion(
10            '{} is not supported in API version < {}'.format(
11                param, min_version
12            )
13        )
14
15    if update_config is not None:
16        if utils.version_lt(version, '1.25'):
17            if 'MaxFailureRatio' in update_config:
18                raise_version_error('UpdateConfig.max_failure_ratio', '1.25')
19            if 'Monitor' in update_config:
20                raise_version_error('UpdateConfig.monitor', '1.25')
21
22        if utils.version_lt(version, '1.28'):
23            if update_config.get('FailureAction') == 'rollback':
24                raise_version_error(
25                    'UpdateConfig.failure_action rollback', '1.28'
26                )
27
28        if utils.version_lt(version, '1.29'):
29            if 'Order' in update_config:
30                raise_version_error('UpdateConfig.order', '1.29')
31
32    if rollback_config is not None:
33        if utils.version_lt(version, '1.28'):
34            raise_version_error('rollback_config', '1.28')
35
36        if utils.version_lt(version, '1.29'):
37            if 'Order' in update_config:
38                raise_version_error('RollbackConfig.order', '1.29')
39
40    if endpoint_spec is not None:
41        if utils.version_lt(version, '1.32') and 'Ports' in endpoint_spec:
42            if any(p.get('PublishMode') for p in endpoint_spec['Ports']):
43                raise_version_error('EndpointSpec.Ports[].mode', '1.32')
44
45    if task_template is not None:
46        if 'ForceUpdate' in task_template and utils.version_lt(
47                version, '1.25'):
48                raise_version_error('force_update', '1.25')
49
50        if task_template.get('Placement'):
51            if utils.version_lt(version, '1.30'):
52                if task_template['Placement'].get('Platforms'):
53                    raise_version_error('Placement.platforms', '1.30')
54            if utils.version_lt(version, '1.27'):
55                if task_template['Placement'].get('Preferences'):
56                    raise_version_error('Placement.preferences', '1.27')
57
58        if task_template.get('ContainerSpec'):
59            container_spec = task_template.get('ContainerSpec')
60
61            if utils.version_lt(version, '1.25'):
62                if container_spec.get('TTY'):
63                    raise_version_error('ContainerSpec.tty', '1.25')
64                if container_spec.get('Hostname') is not None:
65                    raise_version_error('ContainerSpec.hostname', '1.25')
66                if container_spec.get('Hosts') is not None:
67                    raise_version_error('ContainerSpec.hosts', '1.25')
68                if container_spec.get('Groups') is not None:
69                    raise_version_error('ContainerSpec.groups', '1.25')
70                if container_spec.get('DNSConfig') is not None:
71                    raise_version_error('ContainerSpec.dns_config', '1.25')
72                if container_spec.get('Healthcheck') is not None:
73                    raise_version_error('ContainerSpec.healthcheck', '1.25')
74
75            if utils.version_lt(version, '1.28'):
76                if container_spec.get('ReadOnly') is not None:
77                    raise_version_error('ContainerSpec.dns_config', '1.28')
78                if container_spec.get('StopSignal') is not None:
79                    raise_version_error('ContainerSpec.stop_signal', '1.28')
80
81            if utils.version_lt(version, '1.30'):
82                if container_spec.get('Configs') is not None:
83                    raise_version_error('ContainerSpec.configs', '1.30')
84                if container_spec.get('Privileges') is not None:
85                    raise_version_error('ContainerSpec.privileges', '1.30')
86
87            if utils.version_lt(version, '1.35'):
88                if container_spec.get('Isolation') is not None:
89                    raise_version_error('ContainerSpec.isolation', '1.35')
90
91            if utils.version_lt(version, '1.38'):
92                if container_spec.get('Init') is not None:
93                    raise_version_error('ContainerSpec.init', '1.38')
94
95        if task_template.get('Resources'):
96            if utils.version_lt(version, '1.32'):
97                if task_template['Resources'].get('GenericResources'):
98                    raise_version_error('Resources.generic_resources', '1.32')
99
100
101def _merge_task_template(current, override):
102    merged = current.copy()
103    if override is not None:
104        for ts_key, ts_value in override.items():
105            if ts_key == 'ContainerSpec':
106                if 'ContainerSpec' not in merged:
107                    merged['ContainerSpec'] = {}
108                for cs_key, cs_value in override['ContainerSpec'].items():
109                    if cs_value is not None:
110                        merged['ContainerSpec'][cs_key] = cs_value
111            elif ts_value is not None:
112                merged[ts_key] = ts_value
113    return merged
114
115
116class ServiceApiMixin(object):
117    @utils.minimum_version('1.24')
118    def create_service(
119            self, task_template, name=None, labels=None, mode=None,
120            update_config=None, networks=None, endpoint_config=None,
121            endpoint_spec=None, rollback_config=None
122    ):
123        """
124        Create a service.
125
126        Args:
127            task_template (TaskTemplate): Specification of the task to start as
128                part of the new service.
129            name (string): User-defined name for the service. Optional.
130            labels (dict): A map of labels to associate with the service.
131                Optional.
132            mode (ServiceMode): Scheduling mode for the service (replicated
133                or global). Defaults to replicated.
134            update_config (UpdateConfig): Specification for the update strategy
135                of the service. Default: ``None``
136            rollback_config (RollbackConfig): Specification for the rollback
137                strategy of the service. Default: ``None``
138            networks (:py:class:`list`): List of network names or IDs or
139                :py:class:`~docker.types.NetworkAttachmentConfig` to attach the
140                service to. Default: ``None``.
141            endpoint_spec (EndpointSpec): Properties that can be configured to
142                access and load balance a service. Default: ``None``.
143
144        Returns:
145            A dictionary containing an ``ID`` key for the newly created
146            service.
147
148        Raises:
149            :py:class:`docker.errors.APIError`
150                If the server returns an error.
151        """
152
153        _check_api_features(
154            self._version, task_template, update_config, endpoint_spec,
155            rollback_config
156        )
157
158        url = self._url('/services/create')
159        headers = {}
160        image = task_template.get('ContainerSpec', {}).get('Image', None)
161        if image is None:
162            raise errors.DockerException(
163                'Missing mandatory Image key in ContainerSpec'
164            )
165        if mode and not isinstance(mode, dict):
166            mode = ServiceMode(mode)
167
168        registry, repo_name = auth.resolve_repository_name(image)
169        auth_header = auth.get_config_header(self, registry)
170        if auth_header:
171            headers['X-Registry-Auth'] = auth_header
172        if utils.version_lt(self._version, '1.25'):
173            networks = networks or task_template.pop('Networks', None)
174        data = {
175            'Name': name,
176            'Labels': labels,
177            'TaskTemplate': task_template,
178            'Mode': mode,
179            'Networks': utils.convert_service_networks(networks),
180            'EndpointSpec': endpoint_spec
181        }
182
183        if update_config is not None:
184            data['UpdateConfig'] = update_config
185
186        if rollback_config is not None:
187            data['RollbackConfig'] = rollback_config
188
189        return self._result(
190            self._post_json(url, data=data, headers=headers), True
191        )
192
193    @utils.minimum_version('1.24')
194    @utils.check_resource('service')
195    def inspect_service(self, service, insert_defaults=None):
196        """
197        Return information about a service.
198
199        Args:
200            service (str): Service name or ID.
201            insert_defaults (boolean): If true, default values will be merged
202                into the service inspect output.
203
204        Returns:
205            (dict): A dictionary of the server-side representation of the
206                service, including all relevant properties.
207
208        Raises:
209            :py:class:`docker.errors.APIError`
210                If the server returns an error.
211        """
212        url = self._url('/services/{0}', service)
213        params = {}
214        if insert_defaults is not None:
215            if utils.version_lt(self._version, '1.29'):
216                raise errors.InvalidVersion(
217                    'insert_defaults is not supported in API version < 1.29'
218                )
219            params['insertDefaults'] = insert_defaults
220
221        return self._result(self._get(url, params=params), True)
222
223    @utils.minimum_version('1.24')
224    @utils.check_resource('task')
225    def inspect_task(self, task):
226        """
227        Retrieve information about a task.
228
229        Args:
230            task (str): Task ID
231
232        Returns:
233            (dict): Information about the task.
234
235        Raises:
236            :py:class:`docker.errors.APIError`
237                If the server returns an error.
238        """
239        url = self._url('/tasks/{0}', task)
240        return self._result(self._get(url), True)
241
242    @utils.minimum_version('1.24')
243    @utils.check_resource('service')
244    def remove_service(self, service):
245        """
246        Stop and remove a service.
247
248        Args:
249            service (str): Service name or ID
250
251        Returns:
252            ``True`` if successful.
253
254        Raises:
255            :py:class:`docker.errors.APIError`
256                If the server returns an error.
257        """
258
259        url = self._url('/services/{0}', service)
260        resp = self._delete(url)
261        self._raise_for_status(resp)
262        return True
263
264    @utils.minimum_version('1.24')
265    def services(self, filters=None):
266        """
267        List services.
268
269        Args:
270            filters (dict): Filters to process on the nodes list. Valid
271                filters: ``id``, ``name`` , ``label`` and ``mode``.
272                Default: ``None``.
273
274        Returns:
275            A list of dictionaries containing data about each service.
276
277        Raises:
278            :py:class:`docker.errors.APIError`
279                If the server returns an error.
280        """
281        params = {
282            'filters': utils.convert_filters(filters) if filters else None
283        }
284        url = self._url('/services')
285        return self._result(self._get(url, params=params), True)
286
287    @utils.minimum_version('1.25')
288    @utils.check_resource('service')
289    def service_logs(self, service, details=False, follow=False, stdout=False,
290                     stderr=False, since=0, timestamps=False, tail='all',
291                     is_tty=None):
292        """
293            Get log stream for a service.
294            Note: This endpoint works only for services with the ``json-file``
295            or ``journald`` logging drivers.
296
297            Args:
298                service (str): ID or name of the service
299                details (bool): Show extra details provided to logs.
300                    Default: ``False``
301                follow (bool): Keep connection open to read logs as they are
302                    sent by the Engine. Default: ``False``
303                stdout (bool): Return logs from ``stdout``. Default: ``False``
304                stderr (bool): Return logs from ``stderr``. Default: ``False``
305                since (int): UNIX timestamp for the logs staring point.
306                    Default: 0
307                timestamps (bool): Add timestamps to every log line.
308                tail (string or int): Number of log lines to be returned,
309                    counting from the current end of the logs. Specify an
310                    integer or ``'all'`` to output all log lines.
311                    Default: ``all``
312                is_tty (bool): Whether the service's :py:class:`ContainerSpec`
313                    enables the TTY option. If omitted, the method will query
314                    the Engine for the information, causing an additional
315                    roundtrip.
316
317            Returns (generator): Logs for the service.
318        """
319        params = {
320            'details': details,
321            'follow': follow,
322            'stdout': stdout,
323            'stderr': stderr,
324            'since': since,
325            'timestamps': timestamps,
326            'tail': tail
327        }
328
329        url = self._url('/services/{0}/logs', service)
330        res = self._get(url, params=params, stream=True)
331        if is_tty is None:
332            is_tty = self.inspect_service(
333                service
334            )['Spec']['TaskTemplate']['ContainerSpec'].get('TTY', False)
335        return self._get_result_tty(True, res, is_tty)
336
337    @utils.minimum_version('1.24')
338    def tasks(self, filters=None):
339        """
340        Retrieve a list of tasks.
341
342        Args:
343            filters (dict): A map of filters to process on the tasks list.
344                Valid filters: ``id``, ``name``, ``service``, ``node``,
345                ``label`` and ``desired-state``.
346
347        Returns:
348            (:py:class:`list`): List of task dictionaries.
349
350        Raises:
351            :py:class:`docker.errors.APIError`
352                If the server returns an error.
353        """
354
355        params = {
356            'filters': utils.convert_filters(filters) if filters else None
357        }
358        url = self._url('/tasks')
359        return self._result(self._get(url, params=params), True)
360
361    @utils.minimum_version('1.24')
362    @utils.check_resource('service')
363    def update_service(self, service, version, task_template=None, name=None,
364                       labels=None, mode=None, update_config=None,
365                       networks=None, endpoint_config=None,
366                       endpoint_spec=None, fetch_current_spec=False,
367                       rollback_config=None):
368        """
369        Update a service.
370
371        Args:
372            service (string): A service identifier (either its name or service
373                ID).
374            version (int): The version number of the service object being
375                updated. This is required to avoid conflicting writes.
376            task_template (TaskTemplate): Specification of the updated task to
377                start as part of the service.
378            name (string): New name for the service. Optional.
379            labels (dict): A map of labels to associate with the service.
380                Optional.
381            mode (ServiceMode): Scheduling mode for the service (replicated
382                or global). Defaults to replicated.
383            update_config (UpdateConfig): Specification for the update strategy
384                of the service. Default: ``None``.
385            rollback_config (RollbackConfig): Specification for the rollback
386                strategy of the service. Default: ``None``
387            networks (:py:class:`list`): List of network names or IDs or
388                :py:class:`~docker.types.NetworkAttachmentConfig` to attach the
389                service to. Default: ``None``.
390            endpoint_spec (EndpointSpec): Properties that can be configured to
391                access and load balance a service. Default: ``None``.
392            fetch_current_spec (boolean): Use the undefined settings from the
393                current specification of the service. Default: ``False``
394
395        Returns:
396            A dictionary containing a ``Warnings`` key.
397
398        Raises:
399            :py:class:`docker.errors.APIError`
400                If the server returns an error.
401        """
402
403        _check_api_features(
404            self._version, task_template, update_config, endpoint_spec,
405            rollback_config
406        )
407
408        if fetch_current_spec:
409            inspect_defaults = True
410            if utils.version_lt(self._version, '1.29'):
411                inspect_defaults = None
412            current = self.inspect_service(
413                service, insert_defaults=inspect_defaults
414            )['Spec']
415
416        else:
417            current = {}
418
419        url = self._url('/services/{0}/update', service)
420        data = {}
421        headers = {}
422
423        data['Name'] = current.get('Name') if name is None else name
424
425        data['Labels'] = current.get('Labels') if labels is None else labels
426
427        if mode is not None:
428            if not isinstance(mode, dict):
429                mode = ServiceMode(mode)
430            data['Mode'] = mode
431        else:
432            data['Mode'] = current.get('Mode')
433
434        data['TaskTemplate'] = _merge_task_template(
435            current.get('TaskTemplate', {}), task_template
436        )
437
438        container_spec = data['TaskTemplate'].get('ContainerSpec', {})
439        image = container_spec.get('Image', None)
440        if image is not None:
441            registry, repo_name = auth.resolve_repository_name(image)
442            auth_header = auth.get_config_header(self, registry)
443            if auth_header:
444                headers['X-Registry-Auth'] = auth_header
445
446        if update_config is not None:
447            data['UpdateConfig'] = update_config
448        else:
449            data['UpdateConfig'] = current.get('UpdateConfig')
450
451        if rollback_config is not None:
452            data['RollbackConfig'] = rollback_config
453        else:
454            data['RollbackConfig'] = current.get('RollbackConfig')
455
456        if networks is not None:
457            converted_networks = utils.convert_service_networks(networks)
458            if utils.version_lt(self._version, '1.25'):
459                data['Networks'] = converted_networks
460            else:
461                data['TaskTemplate']['Networks'] = converted_networks
462        elif utils.version_lt(self._version, '1.25'):
463            data['Networks'] = current.get('Networks')
464        elif data['TaskTemplate'].get('Networks') is None:
465            current_task_template = current.get('TaskTemplate', {})
466            current_networks = current_task_template.get('Networks')
467            if current_networks is None:
468                current_networks = current.get('Networks')
469            if current_networks is not None:
470                data['TaskTemplate']['Networks'] = current_networks
471
472        if endpoint_spec is not None:
473            data['EndpointSpec'] = endpoint_spec
474        else:
475            data['EndpointSpec'] = current.get('EndpointSpec')
476
477        resp = self._post_json(
478            url, data=data, params={'version': version}, headers=headers
479        )
480        return self._result(resp, json=True)
481