feat(secrets): add --only flag to select secret source
This commit is contained in:
@@ -112,6 +112,13 @@ The command undeploys the affected apps, removes the listed secrets, creates the
|
||||
- the `-e` flag executes post-deployment hooks after each deployment
|
||||
- a secret that is shared between apps has to be listed for every app holding a copy of it, otherwise the old value is copied back from the app that still has it
|
||||
|
||||
Secrets are created in four variants: `conf` (values from the `secrets` configuration), `secret-hooks` (local `abra.sh` commands), `exchange` (`shared_secrets` between apps) and `generate` (everything left over, generated by abra).
|
||||
By default all four run; `-o`/`--only` limits them, both for `reinsert-secrets` and for `secrets`:
|
||||
|
||||
```
|
||||
alakazam example.com.yml secrets --only conf,generate
|
||||
```
|
||||
|
||||
To remove secrets without recreating them, `purge-secrets` uses the same notation:
|
||||
|
||||
```
|
||||
|
||||
+41
-9
@@ -74,6 +74,8 @@ CONNECTION_ERRORS = (
|
||||
)
|
||||
# marker in the abra output that indicates a secret which is not stored on the server
|
||||
MISSING_SECRET_ERROR = "doesn't exist on server"
|
||||
# the variants create_secrets() runs, in the order they are applied
|
||||
SECRET_VARIANTS = ("conf", "secret-hooks", "exchange", "generate")
|
||||
|
||||
|
||||
class MySafeConstructor(SafeConstructor):
|
||||
@@ -1177,22 +1179,26 @@ def config(recipes: Tuple[str]) -> None:
|
||||
@cli.command()
|
||||
@click.option('recipes', '-r', '--recipe', multiple=True, metavar='<RecipeName>', help='Filter for selcted recipes, this option can be specified multiple times.')
|
||||
@click.option('--syncvalues', is_flag=True, default=False, help='Synchronize secret values from source to target when they differ. Without this flag, value mismatches are only reported as warnings.')
|
||||
def secrets(recipes: Tuple[str], syncvalues: bool) -> None:
|
||||
@click.option('only', '-o', '--only', multiple=True, metavar='<conf|secret-hooks|exchange|generate>', help='Only run the listed secret variants, all of them by default. Comma separated or specified multiple times.')
|
||||
def secrets(recipes: Tuple[str], syncvalues: bool, only: Tuple[str]) -> None:
|
||||
"""
|
||||
Generates and inserts secrets for specified apps.
|
||||
This function handles secrets defined in the configuration, secrets produced by secret hooks, shared secrets exchanged between apps and finally all remaining secrets generated by abra.
|
||||
"""
|
||||
create_secrets(recipes, syncvalues=syncvalues)
|
||||
create_secrets(recipes, syncvalues=syncvalues, variants=parse_secret_variants(only))
|
||||
|
||||
|
||||
def create_secrets(recipes: Tuple[str], syncvalues: bool = False) -> None:
|
||||
def create_secrets(recipes: Tuple[str], syncvalues: bool = False, variants: Optional[Set[str]] = None) -> None:
|
||||
"""
|
||||
Creates all secrets that are missing on the server for the selected apps
|
||||
|
||||
Args:
|
||||
recipes (list): Filter for selected recipes; all apps if empty.
|
||||
syncvalues (bool): Synchronize shared secret values from source to target when they differ.
|
||||
variants (set): The variants to run, see SECRET_VARIANTS. Defaults to all of them.
|
||||
"""
|
||||
if variants is None:
|
||||
variants = set(SECRET_VARIANTS)
|
||||
for _, instance_config in INSTANCE_CONFIGS.items():
|
||||
instance_apps = instance_config.keys()
|
||||
if recipes:
|
||||
@@ -1205,14 +1211,18 @@ def create_secrets(recipes: Tuple[str], syncvalues: bool = False) -> None:
|
||||
app_config = instance_config[app]
|
||||
domain = app_config['app_domain']
|
||||
print(f"Create secrets for {domain}")
|
||||
insert_secrets_from_conf(domain, app_config)
|
||||
run_secret_hooks(domain, app_config)
|
||||
if "conf" in variants:
|
||||
insert_secrets_from_conf(domain, app_config)
|
||||
if "secret-hooks" in variants:
|
||||
run_secret_hooks(domain, app_config)
|
||||
# Pass 2: exchange secrets between apps, then generate any remaining missing secrets.
|
||||
for app in selected_apps:
|
||||
app_config = instance_config[app]
|
||||
domain = app_config['app_domain']
|
||||
exchange_secrets(app, instance_config, instance_apps, syncvalues=syncvalues)
|
||||
generate_all_secrets(domain)
|
||||
if "exchange" in variants:
|
||||
exchange_secrets(app, instance_config, instance_apps, syncvalues=syncvalues)
|
||||
if "generate" in variants:
|
||||
generate_all_secrets(domain)
|
||||
|
||||
|
||||
def get_deployed_apps(apps: Tuple[str]) -> Dict[str, str]:
|
||||
@@ -1646,6 +1656,26 @@ def parse_recipe_secrets(secrets: Tuple[str]) -> Dict[str, List[str]]:
|
||||
return recipe_secrets
|
||||
|
||||
|
||||
def parse_secret_variants(only: Tuple[str]) -> Set[str]:
|
||||
"""
|
||||
Parses the --only values into the set of secret variants create_secrets() should run.
|
||||
Every argument may hold several comma separated variants and the option may be repeated, so '--only conf,generate' and '--only conf --only generate' are equivalent.
|
||||
|
||||
Args:
|
||||
only (list): The raw --only arguments; all variants are run if empty.
|
||||
|
||||
Returns:
|
||||
set: The names of the variants to run.
|
||||
|
||||
Raises:
|
||||
click.BadParameter: If a variant is not part of SECRET_VARIANTS.
|
||||
"""
|
||||
variants = {variant.strip() for value in only for variant in value.split(",") if variant.strip()}
|
||||
if unknown := variants - set(SECRET_VARIANTS):
|
||||
raise click.BadParameter(f"unknown secret variant(s) {', '.join(sorted(unknown))}, choose from {', '.join(SECRET_VARIANTS)}")
|
||||
return variants or set(SECRET_VARIANTS)
|
||||
|
||||
|
||||
def purge_app_secrets(recipe_secrets: Dict[str, List[str]]) -> None:
|
||||
"""
|
||||
Removes the given secrets from every app of the given recipes.
|
||||
@@ -1713,14 +1743,16 @@ def format_recipe_secrets(recipe_secrets: Dict[str, List[str]]) -> str:
|
||||
@click.option('secrets', '-s', '--secret', multiple=True, required=True, metavar='<RecipeName>:<SecretName>', help='Secrets to be rotated, assigned to the recipe they belong to. Comma separated or specified multiple times.')
|
||||
@click.option('-e', '--execute-hooks', is_flag=True, help='run post-deployment commands.')
|
||||
@click.option('-c', '--converge-checks', is_flag=True, help='perform convergence checks during deployment.')
|
||||
@click.option('only', '-o', '--only', multiple=True, metavar='<conf|secret-hooks|exchange|generate>', help='Only run the listed secret variants when creating the secrets anew, all of them by default. Comma separated or specified multiple times.')
|
||||
@click.option('noninteractive', '-n', '--non-interactive', is_flag=True, help='Run this command non-interactively')
|
||||
def reinsert_secrets(secrets: Tuple[str], execute_hooks: bool, converge_checks: bool, noninteractive: bool) -> None:
|
||||
def reinsert_secrets(secrets: Tuple[str], execute_hooks: bool, converge_checks: bool, only: Tuple[str], noninteractive: bool) -> None:
|
||||
"""
|
||||
Rotates secrets: undeploys the affected apps, removes the given secrets, creates them anew and redeploys the apps that were deployed before.
|
||||
|
||||
A secret that is shared between apps has to be listed for every app holding a copy of it, otherwise the old value is copied back from the app that still has it.
|
||||
"""
|
||||
recipe_secrets = parse_recipe_secrets(secrets)
|
||||
variants = parse_secret_variants(only)
|
||||
recipes = tuple(recipe_secrets)
|
||||
deployed_apps = get_apps_by_deployment(recipes, deployed=True)
|
||||
print_all_apps(get_apps(recipes))
|
||||
@@ -1728,7 +1760,7 @@ def reinsert_secrets(secrets: Tuple[str], execute_hooks: bool, converge_checks:
|
||||
return
|
||||
undeploy_apps(deployed_apps)
|
||||
purge_app_secrets(recipe_secrets)
|
||||
create_secrets(recipes)
|
||||
create_secrets(recipes, variants=variants)
|
||||
deploy_apps(deployed_apps, execute_hooks=execute_hooks, converge_checks=converge_checks)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user