From e6756a7f75f1793dc5fc438b1307318874d2ff2d Mon Sep 17 00:00:00 2001 From: Wiktor Latanowicz Date: Tue, 23 Sep 2025 09:16:45 +0200 Subject: [PATCH] Add support for multi-line secrets and plain-text params --- ecsctrl/cli.py | 20 +++++++---- ecsctrl/dump/secrets.py | 79 +++++++++++++++++++++++++++++++++-------- 2 files changed, 79 insertions(+), 20 deletions(-) diff --git a/ecsctrl/cli.py b/ecsctrl/cli.py index f12634e..516c995 100644 --- a/ecsctrl/cli.py +++ b/ecsctrl/cli.py @@ -296,12 +296,20 @@ def store( ssm = BotoClient("ssm", dry_run=ctx.obj["boto_client"].dry_run) for secret_name, value in spec.items(): - ssm_params = { - "Name": secret_name, - "Value": value, - "Type": "SecureString", - "Overwrite": True, - } + if isinstance(value, str): + ssm_params = { + "Name": secret_name, + "Value": value, + "Type": "SecureString", + "Overwrite": True, + } + else: + ssm_params = { + "Name": secret_name, + "Value": value["Value"], + "Type": value["Type"], + "Overwrite": True, + } click.echo(f"🔑 Storing secret {secret_name}.") response = ssm.call("put_parameter", **ssm_params) click.echo(f"\t✅ done, parameter version: {response['Version']}") diff --git a/ecsctrl/dump/secrets.py b/ecsctrl/dump/secrets.py index 6a21239..675644b 100644 --- a/ecsctrl/dump/secrets.py +++ b/ecsctrl/dump/secrets.py @@ -1,5 +1,14 @@ import re from . import substitute_with_expressions +from dataclasses import dataclass +from typing import Generator, Sequence + + +@dataclass +class Parameter: + name: str + value: str + type: str def list_secrets(ssm): @@ -18,23 +27,65 @@ def list_secrets(ssm): yield parameter -def dump_secrets(ssm, filter=None): +def dump_secrets(ssm, filter=None) -> Generator[Parameter, None, None]: for parameter in list_secrets(ssm): parameter_name = parameter["Name"] - response = ssm.call( - "get_parameter", - Name=parameter_name, - WithDecryption=True, - ) - if filter is None or re.match(filter, parameter_name): - yield parameter_name, response["Parameter"]["Value"] + response = ssm.call( + "get_parameter", + Name=parameter_name, + WithDecryption=True, + ) + + yield Parameter( + name=parameter_name, + value=response["Parameter"]["Value"], + type=response["Parameter"]["Type"], + ) -def render_dumped_secrets(click, secrets, vars_lut, target_file): +def render_dumped_secrets(click, secrets: Sequence[Parameter], vars_lut, target_file): with open(target_file, "w") as f: - for name, value in secrets: - key = substitute_with_expressions(name, vars_lut) - secret_line = f"{key}: {value}" - f.write(secret_line + "\n") - click.echo(f"🔑 Dumped secret {key}.") + for parameter in secrets: + key, secret_text = rended_single_secret(parameter, vars_lut) + f.write(f"# Dumped from `{parameter.name}`:\n") + f.write(secret_text) + f.write(f"\n") + click.echo(f"🔑 Dumped secret {parameter.name} as {key}.") + + +def rended_single_secret(parameter: Parameter, vars_lut): + name = parameter.name + key = substitute_with_expressions(name, vars_lut) + if parameter.type == "SecureString": + secret_text = render_simple_secret(key, parameter) + else: + secret_text = render_complex_secret(key, parameter) + return key, secret_text + + +def render_simple_secret(key, parameter): + value = render_value(parameter.value, 1) + secret_text = f"{key}: {value}\n" + return secret_text + + +def render_complex_secret(key, parameter): + value = render_value(parameter.value, 2) + secret_text = f"{key}:\n" + secret_text += f" Type: {parameter.type}\n" + secret_text += f" Value: {value}\n" + return secret_text + + +def render_value(value: str, indent: int) -> str: + if "\n" in value: + last_nl = "-" + if value.endswith("\n"): + last_nl = "" + result_lines = [f"|{last_nl}"] + for line in value.split("\n"): + result_lines.append((" " * indent) + line) + return "\n".join(result_lines) + + return value