types.py 6.9 KB

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