Marco Ricci commited on 2024-12-12 11:59:47
Zeige 2 geänderte Dateien mit 68 Einfügungen und 12 Löschungen.
In the "vault" derivation scheme, commands that edit or import a configuration actually merge with the existing configuration, for compatibility with the original vault(1). However, sometimes it is desired to replace the existing configuration instead, when importing or when setting a (service or global) configuration. We introduce a new `--overwrite-existing` command-line option that signals exactly that, and a `--merge-existing` option which signals the (previously implicit) default merging behavior.
... | ... |
@@ -1299,6 +1299,12 @@ class StorageManagementOption(OptionGroupOption): |
1299 | 1299 |
""" |
1300 | 1300 |
|
1301 | 1301 |
|
1302 |
+class CompatibilityOption(OptionGroupOption): |
|
1303 |
+ """Compatibility and incompatibility options for the CLI.""" |
|
1304 |
+ |
|
1305 |
+ option_group_name = 'Options concerning compatibility with other tools' |
|
1306 |
+ |
|
1307 |
+ |
|
1302 | 1308 |
def _validate_occurrence_constraint( |
1303 | 1309 |
ctx: click.Context, |
1304 | 1310 |
param: click.Parameter, |
... | ... |
@@ -1530,6 +1536,13 @@ DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -' |
1530 | 1536 |
help='import saved settings from file PATH', |
1531 | 1537 |
cls=StorageManagementOption, |
1532 | 1538 |
) |
1539 |
+@click.option( |
|
1540 |
+ '--overwrite-existing/--merge-existing', |
|
1541 |
+ 'overwrite_config', |
|
1542 |
+ default=False, |
|
1543 |
+ help='overwrite or merge (default) the existing configuration', |
|
1544 |
+ cls=CompatibilityOption, |
|
1545 |
+) |
|
1533 | 1546 |
@click.version_option(version=dpp.__version__, prog_name=PROG_NAME) |
1534 | 1547 |
@standard_logging_options |
1535 | 1548 |
@click.argument('service', required=False) |
... | ... |
@@ -1556,6 +1569,7 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
1556 | 1569 |
clear_all_settings: bool = False, |
1557 | 1570 |
export_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None, |
1558 | 1571 |
import_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None, |
1572 |
+ overwrite_config: bool = False, |
|
1559 | 1573 |
) -> None: |
1560 | 1574 |
"""Derive a passphrase using the vault(1) derivation scheme. |
1561 | 1575 |
|
... | ... |
@@ -1647,6 +1661,11 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
1647 | 1661 |
must be open for reading and yield `str` values. Otherwise, |
1648 | 1662 |
a filename to open for reading. Using `-` for standard |
1649 | 1663 |
input is supported. |
1664 |
+ overwrite_config: |
|
1665 |
+ Command-line arguments `--overwrite-existing` (True) and |
|
1666 |
+ `--merge-existing` (False). Controls whether config saving |
|
1667 |
+ and config importing overwrite existing configurations, or |
|
1668 |
+ merge them section-wise instead. |
|
1650 | 1669 |
|
1651 | 1670 |
""" # noqa: D301 |
1652 | 1671 |
logger = logging.getLogger(PROG_NAME) |
... | ... |
@@ -1665,6 +1684,8 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
1665 | 1684 |
group = StorageManagementOption |
1666 | 1685 |
elif isinstance(param, LoggingOption): |
1667 | 1686 |
group = LoggingOption |
1687 |
+ elif isinstance(param, CompatibilityOption): |
|
1688 |
+ group = CompatibilityOption |
|
1668 | 1689 |
elif isinstance(param, OptionGroupOption): |
1669 | 1690 |
raise AssertionError( # noqa: DOC501,TRY003,TRY004 |
1670 | 1691 |
f'Unknown option group for {param!r}' # noqa: EM102 |
... | ... |
@@ -1897,8 +1918,12 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
1897 | 1918 |
cast(dict[str, Any], value), |
1898 | 1919 |
form=form, |
1899 | 1920 |
) |
1921 |
+ if overwrite_config: |
|
1922 |
+ put_config(maybe_config) |
|
1923 |
+ else: |
|
1900 | 1924 |
configuration = get_config() |
1901 |
- merged_config: collections.ChainMap[str, Any] = collections.ChainMap( |
|
1925 |
+ merged_config: collections.ChainMap[str, Any] = ( |
|
1926 |
+ collections.ChainMap( |
|
1902 | 1927 |
{ |
1903 | 1928 |
'services': collections.ChainMap( |
1904 | 1929 |
maybe_config['services'], |
... | ... |
@@ -1912,6 +1937,7 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
1912 | 1937 |
if 'global' in configuration |
1913 | 1938 |
else {}, |
1914 | 1939 |
) |
1940 |
+ ) |
|
1915 | 1941 |
new_config: Any = { |
1916 | 1942 |
k: dict(v) if isinstance(v, collections.ChainMap) else v |
1917 | 1943 |
for k, v in sorted(merged_config.items()) |
... | ... |
@@ -2022,10 +2048,14 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
2022 | 2048 |
f'actual settings' |
2023 | 2049 |
) |
2024 | 2050 |
raise click.UsageError(msg) |
2025 |
- if service: |
|
2026 |
- configuration['services'].setdefault(service, {}).update(view) # type: ignore[typeddict-item] |
|
2027 |
- else: |
|
2028 |
- configuration.setdefault('global', {}).update(view) # type: ignore[typeddict-item] |
|
2051 |
+ subtree: dict[str, Any] = ( |
|
2052 |
+ configuration['services'].setdefault(service, {}) # type: ignore[assignment] |
|
2053 |
+ if service |
|
2054 |
+ else configuration.setdefault('global', {}) |
|
2055 |
+ ) |
|
2056 |
+ if overwrite_config: |
|
2057 |
+ subtree.clear() |
|
2058 |
+ subtree.update(view) |
|
2029 | 2059 |
assert _types.is_vault_config( |
2030 | 2060 |
configuration |
2031 | 2061 |
), f'Invalid vault configuration: {configuration!r}' |
... | ... |
@@ -2525,19 +2525,26 @@ class ConfigMergingStateMachine(stateful.RuleBasedStateMachine): |
2525 | 2525 |
|
2526 | 2526 |
@stateful.rule( |
2527 | 2527 |
settings_obj=stateful.consumes(settings), |
2528 |
+ overwrite=strategies.booleans(), |
|
2528 | 2529 |
) |
2529 | 2530 |
def set_globals( |
2530 | 2531 |
self, |
2531 | 2532 |
settings_obj: _types.VaultConfigGlobalSettings, |
2533 |
+ overwrite: bool, |
|
2532 | 2534 |
) -> None: |
2533 |
- self.current_config['global'] = settings_obj |
|
2535 |
+ if overwrite: |
|
2536 |
+ self.current_config['global'] = {} |
|
2537 |
+ self.current_config.setdefault('global', {}).update(settings_obj) |
|
2534 | 2538 |
assert _types.is_vault_config(self.current_config) |
2535 | 2539 |
# NOTE: This relies on settings_obj containing only the keys |
2536 | 2540 |
# "length", "repeat", "upper", "lower", "number", "space", |
2537 | 2541 |
# "dash" and "symbol". |
2538 | 2542 |
_result = self.runner.invoke( |
2539 | 2543 |
cli.derivepassphrase_vault, |
2540 |
- ['--config'] |
|
2544 |
+ [ |
|
2545 |
+ '--config', |
|
2546 |
+ '--overwrite-existing' if overwrite else '--merge-existing', |
|
2547 |
+ ] |
|
2541 | 2548 |
+ [f'--{key}={value}' for key, value in settings_obj.items()], |
2542 | 2549 |
catch_exceptions=False, |
2543 | 2550 |
) |
... | ... |
@@ -2556,20 +2563,29 @@ class ConfigMergingStateMachine(stateful.RuleBasedStateMachine): |
2556 | 2563 |
@stateful.rule( |
2557 | 2564 |
service=known_services, |
2558 | 2565 |
settings_obj=stateful.consumes(settings), |
2566 |
+ overwrite=strategies.booleans(), |
|
2559 | 2567 |
) |
2560 | 2568 |
def set_service( |
2561 | 2569 |
self, |
2562 | 2570 |
service: str, |
2563 | 2571 |
settings_obj: _types.VaultConfigServicesSettings, |
2572 |
+ overwrite: bool, |
|
2564 | 2573 |
) -> None: |
2565 |
- self.current_config['services'][service] = settings_obj |
|
2574 |
+ if overwrite: |
|
2575 |
+ self.current_config['services'][service] = {} |
|
2576 |
+ self.current_config['services'].setdefault(service, {}).update( |
|
2577 |
+ settings_obj |
|
2578 |
+ ) |
|
2566 | 2579 |
assert _types.is_vault_config(self.current_config) |
2567 | 2580 |
# NOTE: This relies on settings_obj containing only the keys |
2568 | 2581 |
# "length", "repeat", "upper", "lower", "number", "space", |
2569 | 2582 |
# "dash" and "symbol". |
2570 | 2583 |
_result = self.runner.invoke( |
2571 | 2584 |
cli.derivepassphrase_vault, |
2572 |
- ['--config'] |
|
2585 |
+ [ |
|
2586 |
+ '--config', |
|
2587 |
+ '--overwrite-existing' if overwrite else '--merge-existing', |
|
2588 |
+ ] |
|
2573 | 2589 |
+ [f'--{key}={value}' for key, value in settings_obj.items()] |
2574 | 2590 |
+ ['--', service], |
2575 | 2591 |
catch_exceptions=False, |
... | ... |
@@ -2627,13 +2643,23 @@ class ConfigMergingStateMachine(stateful.RuleBasedStateMachine): |
2627 | 2643 |
|
2628 | 2644 |
@stateful.rule( |
2629 | 2645 |
config=stateful.consumes(configurations), |
2646 |
+ overwrite=strategies.booleans(), |
|
2647 |
+ ) |
|
2648 |
+ def import_configuraton( |
|
2649 |
+ self, |
|
2650 |
+ config: _types.VaultConfig, |
|
2651 |
+ overwrite: bool, |
|
2652 |
+ ) -> None: |
|
2653 |
+ self.current_config = ( |
|
2654 |
+ self.fold_configs(config, self.current_config) |
|
2655 |
+ if not overwrite |
|
2656 |
+ else config |
|
2630 | 2657 |
) |
2631 |
- def import_configuraton(self, config: _types.VaultConfig) -> None: |
|
2632 |
- self.current_config = self.fold_configs(config, self.current_config) |
|
2633 | 2658 |
assert _types.is_vault_config(self.current_config) |
2634 | 2659 |
_result = self.runner.invoke( |
2635 | 2660 |
cli.derivepassphrase_vault, |
2636 |
- ['--import', '-'], |
|
2661 |
+ ['--import', '-'] |
|
2662 |
+ + (['--overwrite-existing'] if overwrite else []), |
|
2637 | 2663 |
input=json.dumps(config), |
2638 | 2664 |
catch_exceptions=False, |
2639 | 2665 |
) |
2640 | 2666 |