types.py 5.8 KB

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