types.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  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 ..const import COMPOSEFILE_V1 as V1
  11. from .errors import ConfigurationError
  12. from compose.const import IS_WINDOWS_PLATFORM
  13. from compose.utils import splitdrive
  14. win32_root_path_pattern = re.compile(r'^[A-Za-z]\:\\.*')
  15. class VolumeFromSpec(namedtuple('_VolumeFromSpec', 'source mode type')):
  16. # TODO: drop service_names arg when v1 is removed
  17. @classmethod
  18. def parse(cls, volume_from_config, service_names, version):
  19. func = cls.parse_v1 if version == V1 else cls.parse_v2
  20. return func(service_names, volume_from_config)
  21. @classmethod
  22. def parse_v1(cls, service_names, volume_from_config):
  23. parts = volume_from_config.split(':')
  24. if len(parts) > 2:
  25. raise ConfigurationError(
  26. "volume_from {} has incorrect format, should be "
  27. "service[:mode]".format(volume_from_config))
  28. if len(parts) == 1:
  29. source = parts[0]
  30. mode = 'rw'
  31. else:
  32. source, mode = parts
  33. type = 'service' if source in service_names else 'container'
  34. return cls(source, mode, type)
  35. @classmethod
  36. def parse_v2(cls, service_names, volume_from_config):
  37. parts = volume_from_config.split(':')
  38. if len(parts) > 3:
  39. raise ConfigurationError(
  40. "volume_from {} has incorrect format, should be one of "
  41. "'<service name>[:<mode>]' or "
  42. "'container:<container name>[:<mode>]'".format(volume_from_config))
  43. if len(parts) == 1:
  44. source = parts[0]
  45. return cls(source, 'rw', 'service')
  46. if len(parts) == 2:
  47. if parts[0] == 'container':
  48. type, source = parts
  49. return cls(source, 'rw', type)
  50. source, mode = parts
  51. return cls(source, mode, 'service')
  52. if len(parts) == 3:
  53. type, source, mode = parts
  54. if type not in ('service', 'container'):
  55. raise ConfigurationError(
  56. "Unknown volumes_from type '{}' in '{}'".format(
  57. type,
  58. volume_from_config))
  59. return cls(source, mode, type)
  60. def repr(self):
  61. return '{v.type}:{v.source}:{v.mode}'.format(v=self)
  62. def parse_restart_spec(restart_config):
  63. if not restart_config:
  64. return None
  65. parts = restart_config.split(':')
  66. if len(parts) > 2:
  67. raise ConfigurationError(
  68. "Restart %s has incorrect format, should be "
  69. "mode[:max_retry]" % restart_config)
  70. if len(parts) == 2:
  71. name, max_retry_count = parts
  72. else:
  73. name, = parts
  74. max_retry_count = 0
  75. return {'Name': name, 'MaximumRetryCount': int(max_retry_count)}
  76. def serialize_restart_spec(restart_spec):
  77. if not restart_spec:
  78. return ''
  79. parts = [restart_spec['Name']]
  80. if restart_spec['MaximumRetryCount']:
  81. parts.append(six.text_type(restart_spec['MaximumRetryCount']))
  82. return ':'.join(parts)
  83. def parse_extra_hosts(extra_hosts_config):
  84. if not extra_hosts_config:
  85. return {}
  86. if isinstance(extra_hosts_config, dict):
  87. return dict(extra_hosts_config)
  88. if isinstance(extra_hosts_config, list):
  89. extra_hosts_dict = {}
  90. for extra_hosts_line in extra_hosts_config:
  91. # TODO: validate string contains ':' ?
  92. host, ip = extra_hosts_line.split(':', 1)
  93. extra_hosts_dict[host.strip()] = ip.strip()
  94. return extra_hosts_dict
  95. def normalize_path_for_engine(path):
  96. """Windows paths, c:\my\path\shiny, need to be changed to be compatible with
  97. the Engine. Volume paths are expected to be linux style /c/my/path/shiny/
  98. """
  99. drive, tail = splitdrive(path)
  100. if drive:
  101. path = '/' + drive.lower().rstrip(':') + tail
  102. return path.replace('\\', '/')
  103. class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):
  104. @classmethod
  105. def _parse_unix(cls, volume_config):
  106. parts = volume_config.split(':')
  107. if len(parts) > 3:
  108. raise ConfigurationError(
  109. "Volume %s has incorrect format, should be "
  110. "external:internal[:mode]" % volume_config)
  111. if len(parts) == 1:
  112. external = None
  113. internal = os.path.normpath(parts[0])
  114. else:
  115. external = os.path.normpath(parts[0])
  116. internal = os.path.normpath(parts[1])
  117. mode = 'rw'
  118. if len(parts) == 3:
  119. mode = parts[2]
  120. return cls(external, internal, mode)
  121. @classmethod
  122. def _parse_win32(cls, volume_config, normalize):
  123. # relative paths in windows expand to include the drive, eg C:\
  124. # so we join the first 2 parts back together to count as one
  125. mode = 'rw'
  126. def separate_next_section(volume_config):
  127. drive, tail = splitdrive(volume_config)
  128. parts = tail.split(':', 1)
  129. if drive:
  130. parts[0] = drive + parts[0]
  131. return parts
  132. parts = separate_next_section(volume_config)
  133. if len(parts) == 1:
  134. internal = parts[0]
  135. external = None
  136. else:
  137. external = parts[0]
  138. parts = separate_next_section(parts[1])
  139. external = os.path.normpath(external)
  140. internal = parts[0]
  141. if len(parts) > 1:
  142. if ':' in parts[1]:
  143. raise ConfigurationError(
  144. "Volume %s has incorrect format, should be "
  145. "external:internal[:mode]" % volume_config
  146. )
  147. mode = parts[1]
  148. if normalize:
  149. external = normalize_path_for_engine(external) if external else None
  150. return cls(external, internal, mode)
  151. @classmethod
  152. def parse(cls, volume_config, normalize=False):
  153. """Parse a volume_config path and split it into external:internal[:mode]
  154. parts to be returned as a valid VolumeSpec.
  155. """
  156. if IS_WINDOWS_PLATFORM:
  157. return cls._parse_win32(volume_config, normalize)
  158. else:
  159. return cls._parse_unix(volume_config)
  160. def repr(self):
  161. external = self.external + ':' if self.external else ''
  162. return '{ext}{v.internal}:{v.mode}'.format(ext=external, v=self)
  163. @property
  164. def is_named_volume(self):
  165. res = self.external and not self.external.startswith(('.', '/', '~'))
  166. if not IS_WINDOWS_PLATFORM:
  167. return res
  168. return (
  169. res and not self.external.startswith('\\') and
  170. not win32_root_path_pattern.match(self.external)
  171. )
  172. class ServiceLink(namedtuple('_ServiceLink', 'target alias')):
  173. @classmethod
  174. def parse(cls, link_spec):
  175. target, _, alias = link_spec.partition(':')
  176. if not alias:
  177. alias = target
  178. return cls(target, alias)
  179. def repr(self):
  180. if self.target == self.alias:
  181. return self.target
  182. return '{s.target}:{s.alias}'.format(s=self)
  183. @property
  184. def merge_field(self):
  185. return self.alias
  186. class ServiceSecret(namedtuple('_ServiceSecret', 'source target uid gid mode')):
  187. @classmethod
  188. def parse(cls, spec):
  189. if isinstance(spec, six.string_types):
  190. return cls(spec, None, None, None, None)
  191. return cls(
  192. spec.get('source'),
  193. spec.get('target'),
  194. spec.get('uid'),
  195. spec.get('gid'),
  196. spec.get('mode'),
  197. )
  198. @property
  199. def merge_field(self):
  200. return self.source
  201. def repr(self):
  202. return dict(
  203. [(k, v) for k, v in self._asdict().items() if v is not None]
  204. )