interpolation.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. from __future__ import absolute_import
  2. from __future__ import unicode_literals
  3. import logging
  4. import re
  5. from string import Template
  6. import six
  7. from .errors import ConfigurationError
  8. from compose.const import COMPOSEFILE_V2_0 as V2_0
  9. log = logging.getLogger(__name__)
  10. class Interpolator(object):
  11. def __init__(self, templater, mapping):
  12. self.templater = templater
  13. self.mapping = mapping
  14. def interpolate(self, string):
  15. try:
  16. return self.templater(string).substitute(self.mapping)
  17. except ValueError:
  18. raise InvalidInterpolation(string)
  19. def interpolate_environment_variables(version, config, section, environment):
  20. if version <= V2_0:
  21. interpolator = Interpolator(Template, environment)
  22. else:
  23. interpolator = Interpolator(TemplateWithDefaults, environment)
  24. def process_item(name, config_dict):
  25. return dict(
  26. (key, interpolate_value(name, key, val, section, interpolator))
  27. for key, val in (config_dict or {}).items()
  28. )
  29. return dict(
  30. (name, process_item(name, config_dict or {}))
  31. for name, config_dict in config.items()
  32. )
  33. def get_config_path(config_key, section, name):
  34. return '{}.{}.{}'.format(section, name, config_key)
  35. def interpolate_value(name, config_key, value, section, interpolator):
  36. try:
  37. return recursive_interpolate(value, interpolator, get_config_path(config_key, section, name))
  38. except InvalidInterpolation as e:
  39. raise ConfigurationError(
  40. 'Invalid interpolation format for "{config_key}" option '
  41. 'in {section} "{name}": "{string}"'.format(
  42. config_key=config_key,
  43. name=name,
  44. section=section,
  45. string=e.string))
  46. def recursive_interpolate(obj, interpolator, config_path):
  47. def append(config_path, key):
  48. return '{}.{}'.format(config_path, key)
  49. if isinstance(obj, six.string_types):
  50. return converter.convert(config_path, interpolator.interpolate(obj))
  51. if isinstance(obj, dict):
  52. return dict(
  53. (key, recursive_interpolate(val, interpolator, append(config_path, key)))
  54. for (key, val) in obj.items()
  55. )
  56. if isinstance(obj, list):
  57. return [recursive_interpolate(val, interpolator, config_path) for val in obj]
  58. return obj
  59. class TemplateWithDefaults(Template):
  60. idpattern = r'[_a-z][_a-z0-9]*(?::?-[^}]*)?'
  61. # Modified from python2.7/string.py
  62. def substitute(self, mapping):
  63. # Helper function for .sub()
  64. def convert(mo):
  65. # Check the most common path first.
  66. named = mo.group('named') or mo.group('braced')
  67. if named is not None:
  68. if ':-' in named:
  69. var, _, default = named.partition(':-')
  70. return mapping.get(var) or default
  71. if '-' in named:
  72. var, _, default = named.partition('-')
  73. return mapping.get(var, default)
  74. val = mapping[named]
  75. return '%s' % (val,)
  76. if mo.group('escaped') is not None:
  77. return self.delimiter
  78. if mo.group('invalid') is not None:
  79. self._invalid(mo)
  80. raise ValueError('Unrecognized named group in pattern',
  81. self.pattern)
  82. return self.pattern.sub(convert, self.template)
  83. class InvalidInterpolation(Exception):
  84. def __init__(self, string):
  85. self.string = string
  86. PATH_JOKER = '[^.]+'
  87. def re_path(*args):
  88. return re.compile('^{}$'.format('.'.join(args)))
  89. def re_path_basic(section, name):
  90. return re_path(section, PATH_JOKER, name)
  91. def service_path(*args):
  92. return re_path('service', PATH_JOKER, *args)
  93. def to_boolean(s):
  94. s = s.lower()
  95. if s in ['y', 'yes', 'true', 'on']:
  96. return True
  97. elif s in ['n', 'no', 'false', 'off']:
  98. return False
  99. raise ValueError('"{}" is not a valid boolean value'.format(s))
  100. def to_int(s):
  101. # We must be able to handle octal representation for `mode` values notably
  102. if six.PY3 and re.match('^0[0-9]+$', s.strip()):
  103. s = '0o' + s[1:]
  104. return int(s, base=0)
  105. class ConversionMap(object):
  106. map = {
  107. service_path('blkio_config', 'weight'): to_int,
  108. service_path('blkio_config', 'weight_device', 'weight'): to_int,
  109. service_path('cpus'): float,
  110. service_path('cpu_count'): to_int,
  111. service_path('configs', 'mode'): to_int,
  112. service_path('secrets', 'mode'): to_int,
  113. service_path('healthcheck', 'retries'): to_int,
  114. service_path('healthcheck', 'disable'): to_boolean,
  115. service_path('deploy', 'replicas'): to_int,
  116. service_path('deploy', 'update_config', 'parallelism'): to_int,
  117. service_path('deploy', 'update_config', 'max_failure_ratio'): float,
  118. service_path('deploy', 'restart_policy', 'max_attempts'): to_int,
  119. service_path('mem_swappiness'): to_int,
  120. service_path('oom_score_adj'): to_int,
  121. service_path('ports', 'target'): to_int,
  122. service_path('ports', 'published'): to_int,
  123. service_path('scale'): to_int,
  124. service_path('ulimits', PATH_JOKER): to_int,
  125. service_path('ulimits', PATH_JOKER, 'soft'): to_int,
  126. service_path('ulimits', PATH_JOKER, 'hard'): to_int,
  127. service_path('privileged'): to_boolean,
  128. service_path('read_only'): to_boolean,
  129. service_path('stdin_open'): to_boolean,
  130. service_path('tty'): to_boolean,
  131. service_path('volumes', 'read_only'): to_boolean,
  132. service_path('volumes', 'volume', 'nocopy'): to_boolean,
  133. re_path_basic('network', 'attachable'): to_boolean,
  134. re_path_basic('network', 'external'): to_boolean,
  135. re_path_basic('network', 'internal'): to_boolean,
  136. re_path_basic('volume', 'external'): to_boolean,
  137. re_path_basic('secret', 'external'): to_boolean,
  138. re_path_basic('config', 'external'): to_boolean,
  139. }
  140. def convert(self, path, value):
  141. for rexp in self.map.keys():
  142. if rexp.match(path):
  143. return self.map[rexp](value)
  144. return value
  145. converter = ConversionMap()