interpolation_test.py 14 KB

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