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