types.py 12 KB

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