Marco Ricci commited on 2024-12-19 15:04:11
Zeige 4 geänderte Dateien mit 92 Einfügungen und 31 Löschungen.
Introduce a new file for centralized user configuration of `derivepassphrase`, which we expect the user to write and maintain, and which we will never attempt to write to. Treat the existing "vault" configuration file as a "data" file, which is `derivepassphrase`'s responsibility to write and to maintain, and which the user shouldn't edit directly. In general, we now divide the configuration file set semantically into "user" files and "data" files with the aforementioned responsibilities: "user" files are managed by the user, "data" files are managed by the application. (This mirrors the distinction in the XDG Base Directory Specification, although we do not use their paths or their environment variables.) We choose TOML as the file format for "user" files, because it is the simplest configuration file format with both a robust parser in the Python standard library and support for comments. The "data" files currently use JSON, but may use other serialization formats in the future as well. We envision some use cases for the user configuration file such as disabling certain expected warnings, or declaring the desired Unicode normalization form of a text string, but as of now, no such configuration is defined or supported.
... | ... |
@@ -33,6 +33,9 @@ dependencies = [ |
33 | 33 |
# available in older Pythons (such as typing.Self). These are loaded from |
34 | 34 |
# typing_extensions, instead of using explicit version guards. |
35 | 35 |
"typing_extensions", |
36 |
+ # We read configuration files in JSON and TOML format. The latter is |
|
37 |
+ # unavailable in the Python standard library until Python 3.11. |
|
38 |
+ 'tomli; python_version < "3.11"' |
|
36 | 39 |
] |
37 | 40 |
dynamic = ['version'] |
38 | 41 |
|
... | ... |
@@ -17,6 +17,7 @@ import inspect |
17 | 17 |
import json |
18 | 18 |
import logging |
19 | 19 |
import os |
20 |
+import sys |
|
20 | 21 |
import unicodedata |
21 | 22 |
import warnings |
22 | 23 |
from typing import ( |
... | ... |
@@ -40,6 +41,11 @@ from typing_extensions import ( |
40 | 41 |
import derivepassphrase as dpp |
41 | 42 |
from derivepassphrase import _types, exporter, ssh_agent, vault |
42 | 43 |
|
44 |
+if sys.version_info >= (3, 11): |
|
45 |
+ import tomllib |
|
46 |
+else: |
|
47 |
+ import tomli as tomllib |
|
48 |
+ |
|
43 | 49 |
if TYPE_CHECKING: |
44 | 50 |
import pathlib |
45 | 51 |
import socket |
... | ... |
@@ -914,6 +920,8 @@ def _config_filename( |
914 | 920 |
return path |
915 | 921 |
elif subsystem == 'vault': # noqa: RET505 |
916 | 922 |
filename = f'{subsystem}.json' |
923 |
+ elif subsystem == 'user configuration': |
|
924 |
+ filename = 'config.toml' |
|
917 | 925 |
elif subsystem == 'old settings.json': |
918 | 926 |
filename = 'settings.json' |
919 | 927 |
else: # pragma: no cover |
... | ... |
@@ -1013,6 +1021,27 @@ def _save_config(config: _types.VaultConfig, /) -> None: |
1013 | 1021 |
json.dump(config, fileobj) |
1014 | 1022 |
|
1015 | 1023 |
|
1024 |
+def _load_user_config() -> dict[str, Any]: |
|
1025 |
+ """Load the user config from the application directory. |
|
1026 |
+ |
|
1027 |
+ The filename is obtained via [`_config_filename`][]. |
|
1028 |
+ |
|
1029 |
+ Returns: |
|
1030 |
+ The user configuration, as a nested `dict`. |
|
1031 |
+ |
|
1032 |
+ Raises: |
|
1033 |
+ OSError: |
|
1034 |
+ There was an OS error accessing the file. |
|
1035 |
+ ValueError: |
|
1036 |
+ The data loaded from the file is not a valid configuration |
|
1037 |
+ file. |
|
1038 |
+ |
|
1039 |
+ """ |
|
1040 |
+ filename = _config_filename(subsystem='user configuration') |
|
1041 |
+ with open(filename, 'rb') as fileobj: |
|
1042 |
+ return tomllib.load(fileobj) |
|
1043 |
+ |
|
1044 |
+ |
|
1016 | 1045 |
def _get_suitable_ssh_keys( |
1017 | 1046 |
conn: ssh_agent.SSHAgentClient | socket.socket | None = None, / |
1018 | 1047 |
) -> Iterator[_types.KeyCommentPair]: |
... | ... |
@@ -1780,6 +1809,16 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
1780 | 1809 |
except Exception as exc: # noqa: BLE001 |
1781 | 1810 |
err('Cannot store config: %s', str(exc), exc_info=exc) |
1782 | 1811 |
|
1812 |
+ def get_user_config() -> dict[str, Any]: |
|
1813 |
+ try: |
|
1814 |
+ return _load_user_config() |
|
1815 |
+ except FileNotFoundError: |
|
1816 |
+ return {} |
|
1817 |
+ except OSError as e: |
|
1818 |
+ err('Cannot load user config: %s: %r', e.strerror, e.filename) |
|
1819 |
+ except Exception as e: # noqa: BLE001 |
|
1820 |
+ err('Cannot load user config: %s', str(e), exc_info=e) |
|
1821 |
+ |
|
1783 | 1822 |
configuration: _types.VaultConfig |
1784 | 1823 |
|
1785 | 1824 |
check_incompatible_options('--phrase', '--key') |
... | ... |
@@ -1822,6 +1861,8 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
1822 | 1861 |
msg = f'{opt_str} does not take a SERVICE argument' |
1823 | 1862 |
raise click.UsageError(msg) |
1824 | 1863 |
|
1864 |
+ user_config = get_user_config() |
|
1865 |
+ |
|
1825 | 1866 |
if service == '': # noqa: PLC1901 |
1826 | 1867 |
logger.warning( |
1827 | 1868 |
'An empty SERVICE is not supported by vault(1). ' |
... | ... |
@@ -1429,6 +1429,7 @@ def phrase_from_key( |
1429 | 1429 |
def isolated_config( |
1430 | 1430 |
monkeypatch: pytest.MonkeyPatch, |
1431 | 1431 |
runner: click.testing.CliRunner, |
1432 |
+ main_config_str: str | None = None, |
|
1432 | 1433 |
) -> Iterator[None]: |
1433 | 1434 |
prog_name = cli.PROG_NAME |
1434 | 1435 |
env_name = prog_name.replace(' ', '_').upper() + '_PATH' |
... | ... |
@@ -1445,6 +1446,13 @@ def isolated_config( |
1445 | 1446 |
monkeypatch.delenv(env_name, raising=False) |
1446 | 1447 |
config_dir = cli._config_filename(subsystem=None) |
1447 | 1448 |
os.makedirs(config_dir, exist_ok=True) |
1449 |
+ if isinstance(main_config_str, str): |
|
1450 |
+ with open( |
|
1451 |
+ cli._config_filename('user configuration'), |
|
1452 |
+ 'w', |
|
1453 |
+ encoding='UTF-8', |
|
1454 |
+ ) as outfile: |
|
1455 |
+ outfile.write(main_config_str) |
|
1448 | 1456 |
yield |
1449 | 1457 |
|
1450 | 1458 |
|
... | ... |
@@ -1452,12 +1460,15 @@ def isolated_config( |
1452 | 1460 |
def isolated_vault_config( |
1453 | 1461 |
monkeypatch: pytest.MonkeyPatch, |
1454 | 1462 |
runner: click.testing.CliRunner, |
1455 |
- config: Any, |
|
1463 |
+ vault_config: Any, |
|
1464 |
+ main_config_str: str | None = None, |
|
1456 | 1465 |
) -> Iterator[None]: |
1457 |
- with isolated_config(monkeypatch=monkeypatch, runner=runner): |
|
1466 |
+ with isolated_config( |
|
1467 |
+ monkeypatch=monkeypatch, runner=runner, main_config_str=main_config_str |
|
1468 |
+ ): |
|
1458 | 1469 |
config_filename = cli._config_filename(subsystem='vault') |
1459 | 1470 |
with open(config_filename, 'w', encoding='UTF-8') as outfile: |
1460 |
- json.dump(config, outfile) |
|
1471 |
+ json.dump(vault_config, outfile) |
|
1461 | 1472 |
yield |
1462 | 1473 |
|
1463 | 1474 |
|
... | ... |
@@ -327,7 +327,7 @@ class TestCLI: |
327 | 327 |
) -> None: |
328 | 328 |
runner = click.testing.CliRunner(mix_stderr=False) |
329 | 329 |
with tests.isolated_vault_config( |
330 |
- monkeypatch=monkeypatch, runner=runner, config=config |
|
330 |
+ monkeypatch=monkeypatch, runner=runner, vault_config=config |
|
331 | 331 |
): |
332 | 332 |
monkeypatch.setattr( |
333 | 333 |
vault.Vault, 'phrase_from_key', tests.phrase_from_key |
... | ... |
@@ -356,7 +356,7 @@ class TestCLI: |
356 | 356 |
with tests.isolated_vault_config( |
357 | 357 |
monkeypatch=monkeypatch, |
358 | 358 |
runner=runner, |
359 |
- config={'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}}, |
|
359 |
+ vault_config={'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}}, |
|
360 | 360 |
): |
361 | 361 |
monkeypatch.setattr( |
362 | 362 |
cli, '_get_suitable_ssh_keys', tests.suitable_ssh_keys |
... | ... |
@@ -420,7 +420,7 @@ class TestCLI: |
420 | 420 |
monkeypatch.setattr(ssh_agent.SSHAgentClient, 'sign', tests.sign) |
421 | 421 |
runner = click.testing.CliRunner(mix_stderr=False) |
422 | 422 |
with tests.isolated_vault_config( |
423 |
- monkeypatch=monkeypatch, runner=runner, config=config |
|
423 |
+ monkeypatch=monkeypatch, runner=runner, vault_config=config |
|
424 | 424 |
): |
425 | 425 |
_result = runner.invoke( |
426 | 426 |
cli.derivepassphrase_vault, |
... | ... |
@@ -450,7 +450,7 @@ class TestCLI: |
450 | 450 |
with tests.isolated_vault_config( |
451 | 451 |
monkeypatch=monkeypatch, |
452 | 452 |
runner=runner, |
453 |
- config={ |
|
453 |
+ vault_config={ |
|
454 | 454 |
'global': {'key': DUMMY_KEY1_B64}, |
455 | 455 |
'services': { |
456 | 456 |
DUMMY_SERVICE: { |
... | ... |
@@ -510,7 +510,7 @@ class TestCLI: |
510 | 510 |
with tests.isolated_vault_config( |
511 | 511 |
monkeypatch=monkeypatch, |
512 | 512 |
runner=runner, |
513 |
- config=config, |
|
513 |
+ vault_config=config, |
|
514 | 514 |
): |
515 | 515 |
_result = runner.invoke( |
516 | 516 |
cli.derivepassphrase_vault, |
... | ... |
@@ -587,7 +587,7 @@ class TestCLI: |
587 | 587 |
with tests.isolated_vault_config( |
588 | 588 |
monkeypatch=monkeypatch, |
589 | 589 |
runner=runner, |
590 |
- config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
590 |
+ vault_config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
591 | 591 |
): |
592 | 592 |
_result = runner.invoke( |
593 | 593 |
cli.derivepassphrase_vault, |
... | ... |
@@ -613,7 +613,7 @@ class TestCLI: |
613 | 613 |
with tests.isolated_vault_config( |
614 | 614 |
monkeypatch=monkeypatch, |
615 | 615 |
runner=runner, |
616 |
- config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
616 |
+ vault_config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
617 | 617 |
): |
618 | 618 |
monkeypatch.setattr( |
619 | 619 |
cli, '_prompt_for_passphrase', tests.auto_prompt |
... | ... |
@@ -644,7 +644,7 @@ class TestCLI: |
644 | 644 |
with tests.isolated_vault_config( |
645 | 645 |
monkeypatch=monkeypatch, |
646 | 646 |
runner=runner, |
647 |
- config={'services': {}}, |
|
647 |
+ vault_config={'services': {}}, |
|
648 | 648 |
): |
649 | 649 |
_result = runner.invoke( |
650 | 650 |
cli.derivepassphrase_vault, |
... | ... |
@@ -727,7 +727,7 @@ class TestCLI: |
727 | 727 |
with tests.isolated_vault_config( |
728 | 728 |
monkeypatch=monkeypatch, |
729 | 729 |
runner=runner, |
730 |
- config={'services': {}}, |
|
730 |
+ vault_config={'services': {}}, |
|
731 | 731 |
): |
732 | 732 |
_result = runner.invoke( |
733 | 733 |
cli.derivepassphrase_vault, |
... | ... |
@@ -766,7 +766,7 @@ class TestCLI: |
766 | 766 |
with tests.isolated_vault_config( |
767 | 767 |
monkeypatch=pytest.MonkeyPatch(), |
768 | 768 |
runner=runner, |
769 |
- config={'services': {}}, |
|
769 |
+ vault_config={'services': {}}, |
|
770 | 770 |
): |
771 | 771 |
_result = runner.invoke( |
772 | 772 |
cli.derivepassphrase_vault, |
... | ... |
@@ -866,7 +866,7 @@ class TestCLI: |
866 | 866 |
) -> None: |
867 | 867 |
runner = click.testing.CliRunner(mix_stderr=False) |
868 | 868 |
with tests.isolated_vault_config( |
869 |
- monkeypatch=monkeypatch, runner=runner, config={} |
|
869 |
+ monkeypatch=monkeypatch, runner=runner, vault_config={} |
|
870 | 870 |
): |
871 | 871 |
_result = runner.invoke( |
872 | 872 |
cli.derivepassphrase_vault, |
... | ... |
@@ -936,6 +936,8 @@ class TestCLI: |
936 | 936 |
result = tests.ReadableResult.parse(_result) |
937 | 937 |
assert result.error_exit( |
938 | 938 |
error='Cannot load config' |
939 |
+ ) or result.error_exit( |
|
940 |
+ error='Cannot load user config' |
|
939 | 941 |
), 'expected error exit and known error message' |
940 | 942 |
|
941 | 943 |
def test_220_edit_notes_successfully( |
... | ... |
@@ -950,7 +952,7 @@ contents go here |
950 | 952 |
with tests.isolated_vault_config( |
951 | 953 |
monkeypatch=monkeypatch, |
952 | 954 |
runner=runner, |
953 |
- config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
955 |
+ vault_config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
954 | 956 |
): |
955 | 957 |
monkeypatch.setattr(click, 'edit', lambda *a, **kw: edit_result) # noqa: ARG005 |
956 | 958 |
_result = runner.invoke( |
... | ... |
@@ -976,7 +978,7 @@ contents go here |
976 | 978 |
with tests.isolated_vault_config( |
977 | 979 |
monkeypatch=monkeypatch, |
978 | 980 |
runner=runner, |
979 |
- config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
981 |
+ vault_config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
980 | 982 |
): |
981 | 983 |
monkeypatch.setattr(click, 'edit', lambda *a, **kw: None) # noqa: ARG005 |
982 | 984 |
_result = runner.invoke( |
... | ... |
@@ -999,7 +1001,7 @@ contents go here |
999 | 1001 |
with tests.isolated_vault_config( |
1000 | 1002 |
monkeypatch=monkeypatch, |
1001 | 1003 |
runner=runner, |
1002 |
- config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
1004 |
+ vault_config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
1003 | 1005 |
): |
1004 | 1006 |
monkeypatch.setattr(click, 'edit', lambda *a, **kw: 'long\ntext') # noqa: ARG005 |
1005 | 1007 |
_result = runner.invoke( |
... | ... |
@@ -1025,7 +1027,7 @@ contents go here |
1025 | 1027 |
with tests.isolated_vault_config( |
1026 | 1028 |
monkeypatch=monkeypatch, |
1027 | 1029 |
runner=runner, |
1028 |
- config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
1030 |
+ vault_config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
1029 | 1031 |
): |
1030 | 1032 |
monkeypatch.setattr(click, 'edit', lambda *a, **kw: '\n\n') # noqa: ARG005 |
1031 | 1033 |
_result = runner.invoke( |
... | ... |
@@ -1096,7 +1098,7 @@ contents go here |
1096 | 1098 |
with tests.isolated_vault_config( |
1097 | 1099 |
monkeypatch=monkeypatch, |
1098 | 1100 |
runner=runner, |
1099 |
- config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
1101 |
+ vault_config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
1100 | 1102 |
): |
1101 | 1103 |
monkeypatch.setattr( |
1102 | 1104 |
cli, '_get_suitable_ssh_keys', tests.suitable_ssh_keys |
... | ... |
@@ -1141,7 +1143,7 @@ contents go here |
1141 | 1143 |
with tests.isolated_vault_config( |
1142 | 1144 |
monkeypatch=monkeypatch, |
1143 | 1145 |
runner=runner, |
1144 |
- config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
1146 |
+ vault_config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
1145 | 1147 |
): |
1146 | 1148 |
monkeypatch.setattr( |
1147 | 1149 |
cli, '_get_suitable_ssh_keys', tests.suitable_ssh_keys |
... | ... |
@@ -1165,7 +1167,7 @@ contents go here |
1165 | 1167 |
with tests.isolated_vault_config( |
1166 | 1168 |
monkeypatch=monkeypatch, |
1167 | 1169 |
runner=runner, |
1168 |
- config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
1170 |
+ vault_config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
1169 | 1171 |
): |
1170 | 1172 |
custom_error = 'custom error message' |
1171 | 1173 |
|
... | ... |
@@ -1193,7 +1195,7 @@ contents go here |
1193 | 1195 |
with tests.isolated_vault_config( |
1194 | 1196 |
monkeypatch=monkeypatch, |
1195 | 1197 |
runner=runner, |
1196 |
- config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
1198 |
+ vault_config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
1197 | 1199 |
): |
1198 | 1200 |
monkeypatch.delenv('SSH_AUTH_SOCK', raising=False) |
1199 | 1201 |
_result = runner.invoke( |
... | ... |
@@ -1214,7 +1216,7 @@ contents go here |
1214 | 1216 |
with tests.isolated_vault_config( |
1215 | 1217 |
monkeypatch=monkeypatch, |
1216 | 1218 |
runner=runner, |
1217 |
- config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
1219 |
+ vault_config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
1218 | 1220 |
): |
1219 | 1221 |
monkeypatch.setenv('SSH_AUTH_SOCK', os.getcwd()) |
1220 | 1222 |
_result = runner.invoke( |
... | ... |
@@ -1237,7 +1239,7 @@ contents go here |
1237 | 1239 |
with tests.isolated_vault_config( |
1238 | 1240 |
monkeypatch=monkeypatch, |
1239 | 1241 |
runner=runner, |
1240 |
- config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
1242 |
+ vault_config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
1241 | 1243 |
): |
1242 | 1244 |
tests.make_file_readonly( |
1243 | 1245 |
cli._config_filename(subsystem='vault'), |
... | ... |
@@ -1261,7 +1263,7 @@ contents go here |
1261 | 1263 |
with tests.isolated_vault_config( |
1262 | 1264 |
monkeypatch=monkeypatch, |
1263 | 1265 |
runner=runner, |
1264 |
- config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
1266 |
+ vault_config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
1265 | 1267 |
): |
1266 | 1268 |
custom_error = 'custom error message' |
1267 | 1269 |
|
... | ... |
@@ -1495,7 +1497,9 @@ contents go here |
1495 | 1497 |
with tests.isolated_vault_config( |
1496 | 1498 |
monkeypatch=monkeypatch, |
1497 | 1499 |
runner=runner, |
1498 |
- config={'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}}, |
|
1500 |
+ vault_config={ |
|
1501 |
+ 'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()} |
|
1502 |
+ }, |
|
1499 | 1503 |
): |
1500 | 1504 |
_result = runner.invoke( |
1501 | 1505 |
cli.derivepassphrase_vault, |
... | ... |
@@ -1517,7 +1521,7 @@ contents go here |
1517 | 1521 |
with tests.isolated_vault_config( |
1518 | 1522 |
monkeypatch=monkeypatch, |
1519 | 1523 |
runner=runner, |
1520 |
- config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
1524 |
+ vault_config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
1521 | 1525 |
): |
1522 | 1526 |
monkeypatch.setenv( |
1523 | 1527 |
'SSH_AUTH_SOCK', "the value doesn't even matter" |
... | ... |
@@ -1559,7 +1563,7 @@ class TestCLIUtils: |
1559 | 1563 |
) -> None: |
1560 | 1564 |
runner = click.testing.CliRunner() |
1561 | 1565 |
with tests.isolated_vault_config( |
1562 |
- monkeypatch=monkeypatch, runner=runner, config=config |
|
1566 |
+ monkeypatch=monkeypatch, runner=runner, vault_config=config |
|
1563 | 1567 |
): |
1564 | 1568 |
config_filename = cli._config_filename(subsystem='vault') |
1565 | 1569 |
with open(config_filename, encoding='UTF-8') as fileobj: |
... | ... |
@@ -1575,7 +1579,7 @@ class TestCLIUtils: |
1575 | 1579 |
with contextlib.ExitStack() as stack: |
1576 | 1580 |
stack.enter_context( |
1577 | 1581 |
tests.isolated_vault_config( |
1578 |
- monkeypatch=monkeypatch, runner=runner, config={} |
|
1582 |
+ monkeypatch=monkeypatch, runner=runner, vault_config={} |
|
1579 | 1583 |
) |
1580 | 1584 |
) |
1581 | 1585 |
stack.enter_context( |
... | ... |
@@ -1862,7 +1866,9 @@ Boo. |
1862 | 1866 |
runner = click.testing.CliRunner(mix_stderr=False) |
1863 | 1867 |
for start_config in [config, result_config]: |
1864 | 1868 |
with tests.isolated_vault_config( |
1865 |
- monkeypatch=monkeypatch, runner=runner, config=start_config |
|
1869 |
+ monkeypatch=monkeypatch, |
|
1870 |
+ runner=runner, |
|
1871 |
+ vault_config=start_config, |
|
1866 | 1872 |
): |
1867 | 1873 |
_result = runner.invoke( |
1868 | 1874 |
cli.derivepassphrase_vault, |
... | ... |
@@ -2424,7 +2430,7 @@ class ConfigManagementStateMachine(stateful.RuleBasedStateMachine): |
2424 | 2430 |
tests.isolated_vault_config( |
2425 | 2431 |
monkeypatch=self.monkeypatch, |
2426 | 2432 |
runner=self.runner, |
2427 |
- config={'services': {}}, |
|
2433 |
+ vault_config={'services': {}}, |
|
2428 | 2434 |
) |
2429 | 2435 |
) |
2430 | 2436 |
|
2431 | 2437 |