types.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  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. parts = [restart_spec['Name']]
  76. if restart_spec['MaximumRetryCount']:
  77. parts.append(six.text_type(restart_spec['MaximumRetryCount']))
  78. return ':'.join(parts)
  79. def parse_extra_hosts(extra_hosts_config):
  80. if not extra_hosts_config:
  81. return {}
  82. if isinstance(extra_hosts_config, dict):
  83. return dict(extra_hosts_config)
  84. if isinstance(extra_hosts_config, list):
  85. extra_hosts_dict = {}
  86. for extra_hosts_line in extra_hosts_config:
  87. # TODO: validate string contains ':' ?
  88. host, ip = extra_hosts_line.split(':', 1)
  89. extra_hosts_dict[host.strip()] = ip.strip()
  90. return extra_hosts_dict
  91. def normalize_path_for_engine(path):
  92. """Windows paths, c:\my\path\shiny, need to be changed to be compatible with
  93. the Engine. Volume paths are expected to be linux style /c/my/path/shiny/
  94. """
  95. drive, tail = splitdrive(path)
  96. if drive:
  97. path = '/' + drive.lower().rstrip(':') + tail
  98. return path.replace('\\', '/')
  99. class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):
  100. @classmethod
  101. def _parse_unix(cls, volume_config):
  102. parts = volume_config.split(':')
  103. if len(parts) > 3:
  104. raise ConfigurationError(
  105. "Volume %s has incorrect format, should be "
  106. "external:internal[:mode]" % volume_config)
  107. if len(parts) == 1:
  108. external = None
  109. internal = os.path.normpath(parts[0])
  110. else:
  111. external = os.path.normpath(parts[0])
  112. internal = os.path.normpath(parts[1])
  113. mode = 'rw'
  114. if len(parts) == 3:
  115. mode = parts[2]
  116. return cls(external, internal, mode)
  117. @classmethod
  118. def _parse_win32(cls, volume_config):
  119. # relative paths in windows expand to include the drive, eg C:\
  120. # so we join the first 2 parts back together to count as one
  121. mode = 'rw'
  122. def separate_next_section(volume_config):
  123. drive, tail = splitdrive(volume_config)
  124. parts = tail.split(':', 1)
  125. if drive:
  126. parts[0] = drive + parts[0]
  127. return parts
  128. parts = separate_next_section(volume_config)
  129. if len(parts) == 1:
  130. internal = normalize_path_for_engine(os.path.normpath(parts[0]))
  131. external = None
  132. else:
  133. external = parts[0]
  134. parts = separate_next_section(parts[1])
  135. external = normalize_path_for_engine(os.path.normpath(external))
  136. internal = normalize_path_for_engine(os.path.normpath(parts[0]))
  137. if len(parts) > 1:
  138. if ':' in parts[1]:
  139. raise ConfigurationError(
  140. "Volume %s has incorrect format, should be "
  141. "external:internal[:mode]" % volume_config
  142. )
  143. mode = parts[1]
  144. return cls(external, internal, mode)
  145. @classmethod
  146. def parse(cls, volume_config):
  147. """Parse a volume_config path and split it into external:internal[:mode]
  148. parts to be returned as a valid VolumeSpec.
  149. """
  150. if IS_WINDOWS_PLATFORM:
  151. return cls._parse_win32(volume_config)
  152. else:
  153. return cls._parse_unix(volume_config)
  154. def repr(self):
  155. external = self.external + ':' if self.external else ''
  156. return '{ext}{v.internal}:{v.mode}'.format(ext=external, v=self)
  157. @property
  158. def is_named_volume(self):
  159. return self.external and not self.external.startswith(('.', '/', '~'))
  160. class ServiceLink(namedtuple('_ServiceLink', 'target alias')):
  161. @classmethod
  162. def parse(cls, link_spec):
  163. target, _, alias = link_spec.partition(':')
  164. if not alias:
  165. alias = target
  166. return cls(target, alias)
  167. def repr(self):
  168. if self.target == self.alias:
  169. return self.target
  170. return '{s.target}:{s.alias}'.format(s=self)
  171. @property
  172. def merge_field(self):
  173. return self.alias