interpolation_test.py 14 KB

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