types.py 5.4 KB

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