types.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  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 VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):
  105. @classmethod
  106. def _parse_unix(cls, volume_config):
  107. parts = volume_config.split(':')
  108. if len(parts) > 3:
  109. raise ConfigurationError(
  110. "Volume %s has incorrect format, should be "
  111. "external:internal[:mode]" % volume_config)
  112. if len(parts) == 1:
  113. external = None
  114. internal = os.path.normpath(parts[0])
  115. else:
  116. external = os.path.normpath(parts[0])
  117. internal = os.path.normpath(parts[1])
  118. mode = 'rw'
  119. if len(parts) == 3:
  120. mode = parts[2]
  121. return cls(external, internal, mode)
  122. @classmethod
  123. def _parse_win32(cls, volume_config, normalize):
  124. # relative paths in windows expand to include the drive, eg C:\
  125. # so we join the first 2 parts back together to count as one
  126. mode = 'rw'
  127. def separate_next_section(volume_config):
  128. drive, tail = splitdrive(volume_config)
  129. parts = tail.split(':', 1)
  130. if drive:
  131. parts[0] = drive + parts[0]
  132. return parts
  133. parts = separate_next_section(volume_config)
  134. if len(parts) == 1:
  135. internal = parts[0]
  136. external = None
  137. else:
  138. external = parts[0]
  139. parts = separate_next_section(parts[1])
  140. external = os.path.normpath(external)
  141. internal = parts[0]
  142. if len(parts) > 1:
  143. if ':' in parts[1]:
  144. raise ConfigurationError(
  145. "Volume %s has incorrect format, should be "
  146. "external:internal[:mode]" % volume_config
  147. )
  148. mode = parts[1]
  149. if normalize:
  150. external = normalize_path_for_engine(external) if external else None
  151. return cls(external, internal, mode)
  152. @classmethod
  153. def parse(cls, volume_config, normalize=False):
  154. """Parse a volume_config path and split it into external:internal[:mode]
  155. parts to be returned as a valid VolumeSpec.
  156. """
  157. if IS_WINDOWS_PLATFORM:
  158. return cls._parse_win32(volume_config, normalize)
  159. else:
  160. return cls._parse_unix(volume_config)
  161. def repr(self):
  162. external = self.external + ':' if self.external else ''
  163. mode = ':' + self.mode if self.external else ''
  164. return '{ext}{v.internal}{mode}'.format(mode=mode, ext=external, v=self)
  165. @property
  166. def is_named_volume(self):
  167. res = self.external and not self.external.startswith(('.', '/', '~'))
  168. if not IS_WINDOWS_PLATFORM:
  169. return res
  170. return (
  171. res and not self.external.startswith('\\') and
  172. not win32_root_path_pattern.match(self.external)
  173. )
  174. class ServiceLink(namedtuple('_ServiceLink', 'target alias')):
  175. @classmethod
  176. def parse(cls, link_spec):
  177. target, _, alias = link_spec.partition(':')
  178. if not alias:
  179. alias = target
  180. return cls(target, alias)
  181. def repr(self):
  182. if self.target == self.alias:
  183. return self.target
  184. return '{s.target}:{s.alias}'.format(s=self)
  185. @property
  186. def merge_field(self):
  187. return self.alias
  188. class ServiceSecret(namedtuple('_ServiceSecret', 'source target uid gid mode')):
  189. @classmethod
  190. def parse(cls, spec):
  191. if isinstance(spec, six.string_types):
  192. return cls(spec, None, None, None, None)
  193. return cls(
  194. spec.get('source'),
  195. spec.get('target'),
  196. spec.get('uid'),
  197. spec.get('gid'),
  198. spec.get('mode'),
  199. )
  200. @property
  201. def merge_field(self):
  202. return self.source
  203. def repr(self):
  204. return dict(
  205. [(k, v) for k, v in self._asdict().items() if v is not None]
  206. )
  207. class ServicePort(namedtuple('_ServicePort', 'target published protocol mode external_ip')):
  208. @classmethod
  209. def parse(cls, spec):
  210. if not isinstance(spec, dict):
  211. result = []
  212. for k, v in build_port_bindings([spec]).items():
  213. if '/' in k:
  214. target, proto = k.split('/', 1)
  215. else:
  216. target, proto = (k, None)
  217. for pub in v:
  218. if pub is None:
  219. result.append(
  220. cls(target, None, proto, None, None)
  221. )
  222. elif isinstance(pub, tuple):
  223. result.append(
  224. cls(target, pub[1], proto, None, pub[0])
  225. )
  226. else:
  227. result.append(
  228. cls(target, pub, proto, None, None)
  229. )
  230. return result
  231. return [cls(
  232. spec.get('target'),
  233. spec.get('published'),
  234. spec.get('protocol'),
  235. spec.get('mode'),
  236. None
  237. )]
  238. @property
  239. def merge_field(self):
  240. return (self.target, self.published)
  241. def repr(self):
  242. return dict(
  243. [(k, v) for k, v in self._asdict().items() if v is not None]
  244. )
  245. def legacy_repr(self):
  246. return normalize_port_dict(self.repr())
  247. def normalize_port_dict(port):
  248. return '{external_ip}{has_ext_ip}{published}{is_pub}{target}/{protocol}'.format(
  249. published=port.get('published', ''),
  250. is_pub=(':' if port.get('published') else ''),
  251. target=port.get('target'),
  252. protocol=port.get('protocol', 'tcp'),
  253. external_ip=port.get('external_ip', ''),
  254. has_ext_ip=(':' if port.get('external_ip') else ''),
  255. )