/compose/config/types.py

https://gitlab.com/wilane/compose · Python · 198 lines · 139 code · 46 blank · 13 comment · 37 complexity · d3f09df3dfb01b4225483adcfe02fba2 MD5 · raw file

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