Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

service.py 19KB

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