interpolation_test.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. from __future__ import absolute_import
  2. from __future__ import unicode_literals
  3. import pytest
  4. from compose.config.environment import Environment
  5. from compose.config.errors import ConfigurationError
  6. from compose.config.interpolation import interpolate_environment_variables
  7. from compose.config.interpolation import Interpolator
  8. from compose.config.interpolation import InvalidInterpolation
  9. from compose.config.interpolation import TemplateWithDefaults
  10. from compose.config.interpolation import UnsetRequiredSubstitution
  11. from compose.const import COMPOSEFILE_V2_0 as V2_0
  12. from compose.const import COMPOSEFILE_V2_3 as V2_3
  13. from compose.const import COMPOSEFILE_V3_4 as V3_4
  14. @pytest.fixture
  15. def mock_env():
  16. return Environment({
  17. 'USER': 'jenny',
  18. 'FOO': 'bar',
  19. 'TRUE': 'True',
  20. 'FALSE': 'OFF',
  21. 'POSINT': '50',
  22. 'NEGINT': '-200',
  23. 'FLOAT': '0.145',
  24. 'MODE': '0600',
  25. })
  26. @pytest.fixture
  27. def variable_mapping():
  28. return Environment({'FOO': 'first', 'BAR': ''})
  29. @pytest.fixture
  30. def defaults_interpolator(variable_mapping):
  31. return Interpolator(TemplateWithDefaults, variable_mapping).interpolate
  32. def test_interpolate_environment_variables_in_services(mock_env):
  33. services = {
  34. 'servicea': {
  35. 'image': 'example:${USER}',
  36. 'volumes': ['$FOO:/target'],
  37. 'logging': {
  38. 'driver': '${FOO}',
  39. 'options': {
  40. 'user': '$USER',
  41. }
  42. }
  43. }
  44. }
  45. expected = {
  46. 'servicea': {
  47. 'image': 'example:jenny',
  48. 'volumes': ['bar:/target'],
  49. 'logging': {
  50. 'driver': 'bar',
  51. 'options': {
  52. 'user': 'jenny',
  53. }
  54. }
  55. }
  56. }
  57. value = interpolate_environment_variables(V2_0, services, 'service', mock_env)
  58. assert value == expected
  59. def test_interpolate_environment_variables_in_volumes(mock_env):
  60. volumes = {
  61. 'data': {
  62. 'driver': '$FOO',
  63. 'driver_opts': {
  64. 'max': 2,
  65. 'user': '${USER}'
  66. }
  67. },
  68. 'other': None,
  69. }
  70. expected = {
  71. 'data': {
  72. 'driver': 'bar',
  73. 'driver_opts': {
  74. 'max': 2,
  75. 'user': 'jenny'
  76. }
  77. },
  78. 'other': {},
  79. }
  80. value = interpolate_environment_variables(V2_0, volumes, 'volume', mock_env)
  81. assert value == expected
  82. def test_interpolate_environment_variables_in_secrets(mock_env):
  83. secrets = {
  84. 'secretservice': {
  85. 'file': '$FOO',
  86. 'labels': {
  87. 'max': 2,
  88. 'user': '${USER}'
  89. }
  90. },
  91. 'other': None,
  92. }
  93. expected = {
  94. 'secretservice': {
  95. 'file': 'bar',
  96. 'labels': {
  97. 'max': 2,
  98. 'user': 'jenny'
  99. }
  100. },
  101. 'other': {},
  102. }
  103. value = interpolate_environment_variables(V3_4, secrets, 'secret', mock_env)
  104. assert value == expected
  105. def test_interpolate_environment_services_convert_types_v2(mock_env):
  106. entry = {
  107. 'service1': {
  108. 'blkio_config': {
  109. 'weight': '${POSINT}',
  110. 'weight_device': [{'file': '/dev/sda1', 'weight': '${POSINT}'}]
  111. },
  112. 'cpus': '${FLOAT}',
  113. 'cpu_count': '$POSINT',
  114. 'healthcheck': {
  115. 'retries': '${POSINT:-3}',
  116. 'disable': '${FALSE}',
  117. 'command': 'true'
  118. },
  119. 'mem_swappiness': '${DEFAULT:-127}',
  120. 'oom_score_adj': '${NEGINT}',
  121. 'scale': '${POSINT}',
  122. 'ulimits': {
  123. 'nproc': '${POSINT}',
  124. 'nofile': {
  125. 'soft': '${POSINT}',
  126. 'hard': '${DEFAULT:-40000}'
  127. },
  128. },
  129. 'privileged': '${TRUE}',
  130. 'read_only': '${DEFAULT:-no}',
  131. 'tty': '${DEFAULT:-N}',
  132. 'stdin_open': '${DEFAULT-on}',
  133. }
  134. }
  135. expected = {
  136. 'service1': {
  137. 'blkio_config': {
  138. 'weight': 50,
  139. 'weight_device': [{'file': '/dev/sda1', 'weight': 50}]
  140. },
  141. 'cpus': 0.145,
  142. 'cpu_count': 50,
  143. 'healthcheck': {
  144. 'retries': 50,
  145. 'disable': False,
  146. 'command': 'true'
  147. },
  148. 'mem_swappiness': 127,
  149. 'oom_score_adj': -200,
  150. 'scale': 50,
  151. 'ulimits': {
  152. 'nproc': 50,
  153. 'nofile': {
  154. 'soft': 50,
  155. 'hard': 40000
  156. },
  157. },
  158. 'privileged': True,
  159. 'read_only': False,
  160. 'tty': False,
  161. 'stdin_open': True,
  162. }
  163. }
  164. value = interpolate_environment_variables(V2_3, entry, 'service', mock_env)
  165. assert value == expected
  166. def test_interpolate_environment_services_convert_types_v3(mock_env):
  167. entry = {
  168. 'service1': {
  169. 'healthcheck': {
  170. 'retries': '${POSINT:-3}',
  171. 'disable': '${FALSE}',
  172. 'command': 'true'
  173. },
  174. 'ulimits': {
  175. 'nproc': '${POSINT}',
  176. 'nofile': {
  177. 'soft': '${POSINT}',
  178. 'hard': '${DEFAULT:-40000}'
  179. },
  180. },
  181. 'privileged': '${TRUE}',
  182. 'read_only': '${DEFAULT:-no}',
  183. 'tty': '${DEFAULT:-N}',
  184. 'stdin_open': '${DEFAULT-on}',
  185. 'deploy': {
  186. 'update_config': {
  187. 'parallelism': '${DEFAULT:-2}',
  188. 'max_failure_ratio': '${FLOAT}',
  189. },
  190. 'restart_policy': {
  191. 'max_attempts': '$POSINT',
  192. },
  193. 'replicas': '${DEFAULT-3}'
  194. },
  195. 'ports': [{'target': '${POSINT}', 'published': '${DEFAULT:-5000}'}],
  196. 'configs': [{'mode': '${MODE}', 'source': 'config1'}],
  197. 'secrets': [{'mode': '${MODE}', 'source': 'secret1'}],
  198. }
  199. }
  200. expected = {
  201. 'service1': {
  202. 'healthcheck': {
  203. 'retries': 50,
  204. 'disable': False,
  205. 'command': 'true'
  206. },
  207. 'ulimits': {
  208. 'nproc': 50,
  209. 'nofile': {
  210. 'soft': 50,
  211. 'hard': 40000
  212. },
  213. },
  214. 'privileged': True,
  215. 'read_only': False,
  216. 'tty': False,
  217. 'stdin_open': True,
  218. 'deploy': {
  219. 'update_config': {
  220. 'parallelism': 2,
  221. 'max_failure_ratio': 0.145,
  222. },
  223. 'restart_policy': {
  224. 'max_attempts': 50,
  225. },
  226. 'replicas': 3
  227. },
  228. 'ports': [{'target': 50, 'published': 5000}],
  229. 'configs': [{'mode': 0o600, 'source': 'config1'}],
  230. 'secrets': [{'mode': 0o600, 'source': 'secret1'}],
  231. }
  232. }
  233. value = interpolate_environment_variables(V3_4, entry, 'service', mock_env)
  234. assert value == expected
  235. def test_interpolate_environment_services_convert_types_invalid(mock_env):
  236. entry = {'service1': {'privileged': '${POSINT}'}}
  237. with pytest.raises(ConfigurationError) as exc:
  238. interpolate_environment_variables(V2_3, entry, 'service', mock_env)
  239. assert 'Error while attempting to convert service.service1.privileged to '\
  240. 'appropriate type: "50" is not a valid boolean value' in exc.exconly()
  241. entry = {'service1': {'cpus': '${TRUE}'}}
  242. with pytest.raises(ConfigurationError) as exc:
  243. interpolate_environment_variables(V2_3, entry, 'service', mock_env)
  244. assert 'Error while attempting to convert service.service1.cpus to '\
  245. 'appropriate type: "True" is not a valid float' in exc.exconly()
  246. entry = {'service1': {'ulimits': {'nproc': '${FLOAT}'}}}
  247. with pytest.raises(ConfigurationError) as exc:
  248. interpolate_environment_variables(V2_3, entry, 'service', mock_env)
  249. assert 'Error while attempting to convert service.service1.ulimits.nproc to '\
  250. 'appropriate type: "0.145" is not a valid integer' in exc.exconly()
  251. def test_interpolate_environment_network_convert_types(mock_env):
  252. entry = {
  253. 'network1': {
  254. 'external': '${FALSE}',
  255. 'attachable': '${TRUE}',
  256. 'internal': '${DEFAULT:-false}'
  257. }
  258. }
  259. expected = {
  260. 'network1': {
  261. 'external': False,
  262. 'attachable': True,
  263. 'internal': False,
  264. }
  265. }
  266. value = interpolate_environment_variables(V3_4, entry, 'network', mock_env)
  267. assert value == expected
  268. def test_interpolate_environment_external_resource_convert_types(mock_env):
  269. entry = {
  270. 'resource1': {
  271. 'external': '${TRUE}',
  272. }
  273. }
  274. expected = {
  275. 'resource1': {
  276. 'external': True,
  277. }
  278. }
  279. value = interpolate_environment_variables(V3_4, entry, 'network', mock_env)
  280. assert value == expected
  281. value = interpolate_environment_variables(V3_4, entry, 'volume', mock_env)
  282. assert value == expected
  283. value = interpolate_environment_variables(V3_4, entry, 'secret', mock_env)
  284. assert value == expected
  285. value = interpolate_environment_variables(V3_4, entry, 'config', mock_env)
  286. assert value == expected
  287. def test_escaped_interpolation(defaults_interpolator):
  288. assert defaults_interpolator('$${foo}') == '${foo}'
  289. def test_invalid_interpolation(defaults_interpolator):
  290. with pytest.raises(InvalidInterpolation):
  291. defaults_interpolator('${')
  292. with pytest.raises(InvalidInterpolation):
  293. defaults_interpolator('$}')
  294. with pytest.raises(InvalidInterpolation):
  295. defaults_interpolator('${}')
  296. with pytest.raises(InvalidInterpolation):
  297. defaults_interpolator('${ }')
  298. with pytest.raises(InvalidInterpolation):
  299. defaults_interpolator('${ foo}')
  300. with pytest.raises(InvalidInterpolation):
  301. defaults_interpolator('${foo }')
  302. with pytest.raises(InvalidInterpolation):
  303. defaults_interpolator('${foo!}')
  304. def test_interpolate_missing_no_default(defaults_interpolator):
  305. assert defaults_interpolator("This ${missing} var") == "This var"
  306. assert defaults_interpolator("This ${BAR} var") == "This var"
  307. def test_interpolate_with_value(defaults_interpolator):
  308. assert defaults_interpolator("This $FOO var") == "This first var"
  309. assert defaults_interpolator("This ${FOO} var") == "This first var"
  310. def test_interpolate_missing_with_default(defaults_interpolator):
  311. assert defaults_interpolator("ok ${missing:-def}") == "ok def"
  312. assert defaults_interpolator("ok ${missing-def}") == "ok def"
  313. def test_interpolate_with_empty_and_default_value(defaults_interpolator):
  314. assert defaults_interpolator("ok ${BAR:-def}") == "ok def"
  315. assert defaults_interpolator("ok ${BAR-def}") == "ok "
  316. def test_interpolate_mandatory_values(defaults_interpolator):
  317. assert defaults_interpolator("ok ${FOO:?bar}") == "ok first"
  318. assert defaults_interpolator("ok ${FOO?bar}") == "ok first"
  319. assert defaults_interpolator("ok ${BAR?bar}") == "ok "
  320. with pytest.raises(UnsetRequiredSubstitution) as e:
  321. defaults_interpolator("not ok ${BAR:?high bar}")
  322. assert e.value.err == 'high bar'
  323. with pytest.raises(UnsetRequiredSubstitution) as e:
  324. defaults_interpolator("not ok ${BAZ?dropped the bazz}")
  325. assert e.value.err == 'dropped the bazz'
  326. def test_interpolate_mandatory_no_err_msg(defaults_interpolator):
  327. with pytest.raises(UnsetRequiredSubstitution) as e:
  328. defaults_interpolator("not ok ${BAZ?}")
  329. assert e.value.err == ''
  330. def test_interpolate_mixed_separators(defaults_interpolator):
  331. assert defaults_interpolator("ok ${BAR:-/non:-alphanumeric}") == "ok /non:-alphanumeric"
  332. assert defaults_interpolator("ok ${BAR:-:?wwegegr??:?}") == "ok :?wwegegr??:?"
  333. assert defaults_interpolator("ok ${BAR-:-hello}") == 'ok '
  334. with pytest.raises(UnsetRequiredSubstitution) as e:
  335. defaults_interpolator("not ok ${BAR:?xazz:-redf}")
  336. assert e.value.err == 'xazz:-redf'
  337. assert defaults_interpolator("ok ${BAR?...:?bar}") == "ok "
  338. def test_unbraced_separators(defaults_interpolator):
  339. assert defaults_interpolator("ok $FOO:-bar") == "ok first:-bar"
  340. assert defaults_interpolator("ok $BAZ?error") == "ok ?error"