types.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. """
  2. Types for objects parsed from the configuration.
  3. """
  4. from __future__ import absolute_import
  5. from __future__ import unicode_literals
  6. import ntpath
  7. import os
  8. import re
  9. from collections import namedtuple
  10. import six
  11. from docker.utils.ports import build_port_bindings
  12. from ..const import COMPOSEFILE_V1 as V1
  13. from .errors import ConfigurationError
  14. from compose.const import IS_WINDOWS_PLATFORM
  15. from compose.utils import splitdrive
  16. win32_root_path_pattern = re.compile(r'^[A-Za-z]\:\\.*')
  17. class VolumeFromSpec(namedtuple('_VolumeFromSpec', 'source mode type')):
  18. # TODO: drop service_names arg when v1 is removed
  19. @classmethod
  20. def parse(cls, volume_from_config, service_names, version):
  21. func = cls.parse_v1 if version == V1 else cls.parse_v2
  22. return func(service_names, volume_from_config)
  23. @classmethod
  24. def parse_v1(cls, service_names, volume_from_config):
  25. parts = volume_from_config.split(':')
  26. if len(parts) > 2:
  27. raise ConfigurationError(
  28. "volume_from {} has incorrect format, should be "
  29. "service[:mode]".format(volume_from_config))
  30. if len(parts) == 1:
  31. source = parts[0]
  32. mode = 'rw'
  33. else:
  34. source, mode = parts
  35. type = 'service' if source in service_names else 'container'
  36. return cls(source, mode, type)
  37. @classmethod
  38. def parse_v2(cls, service_names, volume_from_config):
  39. parts = volume_from_config.split(':')
  40. if len(parts) > 3:
  41. raise ConfigurationError(
  42. "volume_from {} has incorrect format, should be one of "
  43. "'<service name>[:<mode>]' or "
  44. "'container:<container name>[:<mode>]'".format(volume_from_config))
  45. if len(parts) == 1:
  46. source = parts[0]
  47. return cls(source, 'rw', 'service')
  48. if len(parts) == 2:
  49. if parts[0] == 'container':
  50. type, source = parts
  51. return cls(source, 'rw', type)
  52. source, mode = parts
  53. return cls(source, mode, 'service')
  54. if len(parts) == 3:
  55. type, source, mode = parts
  56. if type not in ('service', 'container'):
  57. raise ConfigurationError(
  58. "Unknown volumes_from type '{}' in '{}'".format(
  59. type,
  60. volume_from_config))
  61. return cls(source, mode, type)
  62. def repr(self):
  63. return '{v.type}:{v.source}:{v.mode}'.format(v=self)
  64. def parse_restart_spec(restart_config):
  65. if not restart_config:
  66. return None
  67. parts = restart_config.split(':')
  68. if len(parts) > 2:
  69. raise ConfigurationError(
  70. "Restart %s has incorrect format, should be "
  71. "mode[:max_retry]" % restart_config)
  72. if len(parts) == 2:
  73. name, max_retry_count = parts
  74. else:
  75. name, = parts
  76. max_retry_count = 0
  77. return {'Name': name, 'MaximumRetryCount': int(max_retry_count)}
  78. def serialize_restart_spec(restart_spec):
  79. if not restart_spec:
  80. return ''
  81. parts = [restart_spec['Name']]
  82. if restart_spec['MaximumRetryCount']:
  83. parts.append(six.text_type(restart_spec['MaximumRetryCount']))
  84. return ':'.join(parts)
  85. def parse_extra_hosts(extra_hosts_config):
  86. if not extra_hosts_config:
  87. return {}
  88. if isinstance(extra_hosts_config, dict):
  89. return dict(extra_hosts_config)
  90. if isinstance(extra_hosts_config, list):
  91. extra_hosts_dict = {}
  92. for extra_hosts_line in extra_hosts_config:
  93. # TODO: validate string contains ':' ?
  94. host, ip = extra_hosts_line.split(':', 1)
  95. extra_hosts_dict[host.strip()] = ip.strip()
  96. return extra_hosts_dict
  97. def normalize_path_for_engine(path):
  98. """Windows paths, c:\my\path\shiny, need to be changed to be compatible with
  99. the Engine. Volume paths are expected to be linux style /c/my/path/shiny/
  100. """
  101. drive, tail = splitdrive(path)
  102. if drive:
  103. path = '/' + drive.lower().rstrip(':') + tail
  104. return path.replace('\\', '/')
  105. class MountSpec(object):
  106. options_map = {
  107. 'volume': {
  108. 'nocopy': 'no_copy'
  109. },
  110. 'bind': {
  111. 'propagation': 'propagation'
  112. }
  113. }
  114. _fields = ['type', 'source', 'target', 'read_only', 'consistency']
  115. @classmethod
  116. def parse(cls, mount_dict, normalize=False, win_host=False):
  117. normpath = ntpath.normpath if win_host else os.path.normpath
  118. if mount_dict.get('source'):
  119. mount_dict['source'] = normpath(mount_dict['source'])
  120. if normalize:
  121. mount_dict['source'] = normalize_path_for_engine(mount_dict['source'])
  122. return cls(**mount_dict)
  123. def __init__(self, type, source=None, target=None, read_only=None, consistency=None, **kwargs):
  124. self.type = type
  125. self.source = source
  126. self.target = target
  127. self.read_only = read_only
  128. self.consistency = consistency
  129. self.options = None
  130. if self.type in kwargs:
  131. self.options = kwargs[self.type]
  132. def as_volume_spec(self):
  133. mode = 'ro' if self.read_only else 'rw'
  134. return VolumeSpec(external=self.source, internal=self.target, mode=mode)
  135. def legacy_repr(self):
  136. return self.as_volume_spec().repr()
  137. def repr(self):
  138. res = {}
  139. for field in self._fields:
  140. if getattr(self, field, None):
  141. res[field] = getattr(self, field)
  142. if self.options:
  143. res[self.type] = self.options
  144. return res
  145. @property
  146. def is_named_volume(self):
  147. return self.type == 'volume' and self.source
  148. @property
  149. def is_tmpfs(self):
  150. return self.type == 'tmpfs'
  151. @property
  152. def external(self):
  153. return self.source
  154. class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):
  155. win32 = False
  156. @classmethod
  157. def _parse_unix(cls, volume_config):
  158. parts = volume_config.split(':')
  159. if len(parts) > 3:
  160. raise ConfigurationError(
  161. "Volume %s has incorrect format, should be "
  162. "external:internal[:mode]" % volume_config)
  163. if len(parts) == 1:
  164. external = None
  165. internal = os.path.normpath(parts[0])
  166. else:
  167. external = os.path.normpath(parts[0])
  168. internal = os.path.normpath(parts[1])
  169. mode = 'rw'
  170. if len(parts) == 3:
  171. mode = parts[2]
  172. return cls(external, internal, mode)
  173. @classmethod
  174. def _parse_win32(cls, volume_config, normalize):
  175. # relative paths in windows expand to include the drive, eg C:\
  176. # so we join the first 2 parts back together to count as one
  177. mode = 'rw'
  178. def separate_next_section(volume_config):
  179. drive, tail = splitdrive(volume_config)
  180. parts = tail.split(':', 1)
  181. if drive:
  182. parts[0] = drive + parts[0]
  183. return parts
  184. parts = separate_next_section(volume_config)
  185. if len(parts) == 1:
  186. internal = parts[0]
  187. external = None
  188. else:
  189. external = parts[0]
  190. parts = separate_next_section(parts[1])
  191. external = ntpath.normpath(external)
  192. internal = parts[0]
  193. if len(parts) > 1:
  194. if ':' in parts[1]:
  195. raise ConfigurationError(
  196. "Volume %s has incorrect format, should be "
  197. "external:internal[:mode]" % volume_config
  198. )
  199. mode = parts[1]
  200. if normalize:
  201. external = normalize_path_for_engine(external) if external else None
  202. result = cls(external, internal, mode)
  203. result.win32 = True
  204. return result
  205. @classmethod
  206. def parse(cls, volume_config, normalize=False, win_host=False):
  207. """Parse a volume_config path and split it into external:internal[:mode]
  208. parts to be returned as a valid VolumeSpec.
  209. """
  210. if IS_WINDOWS_PLATFORM or win_host:
  211. return cls._parse_win32(volume_config, normalize)
  212. else:
  213. return cls._parse_unix(volume_config)
  214. def repr(self):
  215. external = self.external + ':' if self.external else ''
  216. mode = ':' + self.mode if self.external else ''
  217. return '{ext}{v.internal}{mode}'.format(mode=mode, ext=external, v=self)
  218. @property
  219. def is_named_volume(self):
  220. res = self.external and not self.external.startswith(('.', '/', '~'))
  221. if not self.win32:
  222. return res
  223. return (
  224. res and not self.external.startswith('\\') and
  225. not win32_root_path_pattern.match(self.external)
  226. )
  227. class ServiceLink(namedtuple('_ServiceLink', 'target alias')):
  228. @classmethod
  229. def parse(cls, link_spec):
  230. target, _, alias = link_spec.partition(':')
  231. if not alias:
  232. alias = target
  233. return cls(target, alias)
  234. def repr(self):
  235. if self.target == self.alias:
  236. return self.target
  237. return '{s.target}:{s.alias}'.format(s=self)
  238. @property
  239. def merge_field(self):
  240. return self.alias
  241. class ServiceConfigBase(namedtuple('_ServiceConfigBase', 'source target uid gid mode name')):
  242. @classmethod
  243. def parse(cls, spec):
  244. if isinstance(spec, six.string_types):
  245. return cls(spec, None, None, None, None, None)
  246. return cls(
  247. spec.get('source'),
  248. spec.get('target'),
  249. spec.get('uid'),
  250. spec.get('gid'),
  251. spec.get('mode'),
  252. spec.get('name')
  253. )
  254. @property
  255. def merge_field(self):
  256. return self.source
  257. def repr(self):
  258. return dict(
  259. [(k, v) for k, v in zip(self._fields, self) if v is not None]
  260. )
  261. class ServiceSecret(ServiceConfigBase):
  262. pass
  263. class ServiceConfig(ServiceConfigBase):
  264. pass
  265. class ServicePort(namedtuple('_ServicePort', 'target published protocol mode external_ip')):
  266. def __new__(cls, target, published, *args, **kwargs):
  267. try:
  268. if target:
  269. target = int(target)
  270. except ValueError:
  271. raise ConfigurationError('Invalid target port: {}'.format(target))
  272. if published:
  273. if isinstance(published, six.string_types) and '-' in published: # "x-y:z" format
  274. a, b = published.split('-', 1)
  275. try:
  276. int(a)
  277. int(b)
  278. except ValueError:
  279. raise ConfigurationError('Invalid published port: {}'.format(published))
  280. else:
  281. try:
  282. published = int(published)
  283. except ValueError:
  284. raise ConfigurationError('Invalid published port: {}'.format(published))
  285. return super(ServicePort, cls).__new__(
  286. cls, target, published, *args, **kwargs
  287. )
  288. @classmethod
  289. def parse(cls, spec):
  290. if isinstance(spec, cls):
  291. # When extending a service with ports, the port definitions have already been parsed
  292. return [spec]
  293. if not isinstance(spec, dict):
  294. result = []
  295. try:
  296. for k, v in build_port_bindings([spec]).items():
  297. if '/' in k:
  298. target, proto = k.split('/', 1)
  299. else:
  300. target, proto = (k, None)
  301. for pub in v:
  302. if pub is None:
  303. result.append(
  304. cls(target, None, proto, None, None)
  305. )
  306. elif isinstance(pub, tuple):
  307. result.append(
  308. cls(target, pub[1], proto, None, pub[0])
  309. )
  310. else:
  311. result.append(
  312. cls(target, pub, proto, None, None)
  313. )
  314. except ValueError as e:
  315. raise ConfigurationError(str(e))
  316. return result
  317. return [cls(
  318. spec.get('target'),
  319. spec.get('published'),
  320. spec.get('protocol'),
  321. spec.get('mode'),
  322. None
  323. )]
  324. @property
  325. def merge_field(self):
  326. return (self.target, self.published, self.external_ip, self.protocol)
  327. def repr(self):
  328. return dict(
  329. [(k, v) for k, v in zip(self._fields, self) if v is not None]
  330. )
  331. def legacy_repr(self):
  332. return normalize_port_dict(self.repr())
  333. class GenericResource(namedtuple('_GenericResource', 'kind value')):
  334. @classmethod
  335. def parse(cls, dct):
  336. if 'discrete_resource_spec' not in dct:
  337. raise ConfigurationError(
  338. 'generic_resource entry must include a discrete_resource_spec key'
  339. )
  340. if 'kind' not in dct['discrete_resource_spec']:
  341. raise ConfigurationError(
  342. 'generic_resource entry must include a discrete_resource_spec.kind subkey'
  343. )
  344. return cls(
  345. dct['discrete_resource_spec']['kind'],
  346. dct['discrete_resource_spec'].get('value')
  347. )
  348. def repr(self):
  349. return {
  350. 'discrete_resource_spec': {
  351. 'kind': self.kind,
  352. 'value': self.value,
  353. }
  354. }
  355. @property
  356. def merge_field(self):
  357. return self.kind
  358. def normalize_port_dict(port):
  359. return '{external_ip}{has_ext_ip}{published}{is_pub}{target}/{protocol}'.format(
  360. published=port.get('published', ''),
  361. is_pub=(':' if port.get('published') is not None or port.get('external_ip') else ''),
  362. target=port.get('target'),
  363. protocol=port.get('protocol', 'tcp'),
  364. external_ip=port.get('external_ip', ''),
  365. has_ext_ip=(':' if port.get('external_ip') else ''),
  366. )