From 0369d8060b4e40e1e1b337e0f598f0dad15602e6 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Thu, 27 Jul 2023 06:32:13 -0400 Subject: [PATCH 01/67] gotta start somewhere --- .../utilities/parameters/base.py | 72 +++++++++++++++- .../utilities/parameters/exceptions.py | 4 + .../utilities/parameters/secrets.py | 84 +++++++++++++++++++ 3 files changed, 157 insertions(+), 3 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/base.py b/aws_lambda_powertools/utilities/parameters/base.py index 4357b5d520e..9efc86aba6f 100644 --- a/aws_lambda_powertools/utilities/parameters/base.py +++ b/aws_lambda_powertools/utilities/parameters/base.py @@ -29,7 +29,7 @@ from aws_lambda_powertools.shared.functions import resolve_max_age from aws_lambda_powertools.utilities.parameters.types import TransformOptions -from .exceptions import GetParameterError, TransformParameterError +from .exceptions import GetParameterError, TransformParameterError, UpdateSecretError if TYPE_CHECKING: from mypy_boto3_appconfigdata import AppConfigDataClient @@ -133,9 +133,8 @@ def get( try: value = self._get(name, **sdk_options) - # Encapsulate all errors into a generic GetParameterError except Exception as exc: - raise GetParameterError(str(exc)) + raise GetParameterError(str(exc)) from exc if transform: value = transform_value(key=name, value=value, transform=transform, raise_on_transform_error=True) @@ -228,6 +227,70 @@ def add_to_cache(self, key: Tuple[str, TransformOptions], value: Any, max_age: i self.store[key] = ExpirableValue(value, datetime.now() + timedelta(seconds=max_age)) + def update( + self, + name: str, + transform: TransformOptions = None, + # force_fetch: bool = False, + **sdk_options, + ) -> Optional[Union[str, dict, bytes]]: + """ + Modifies the details of a secret, including metadata and the secret value. + + Parameters + ---------- + name: str + Parameter name + transform: str + Optional transformation of the parameter value. Supported values + are "json" for JSON strings and "binary" for base 64 encoded + values. + force_fetch: bool, optional + Force update even before a cached item has expired, defaults to False + sdk_options: dict, optional + Arguments that will be passed directly to the underlying API call + + Raises + ------ + GetParameterError + When the parameter provider fails to retrieve a parameter value for + a given name. + TransformParameterError + When the parameter provider fails to transform a parameter value. + """ + + # If there are multiple calls to the same parameter but in a different + # transform, they will be stored multiple times. This allows us to + # optimize by transforming the data only once per retrieval, thus there + # is no need to transform cached values multiple times. However, this + # means that we need to make multiple calls to the underlying parameter + # store if we need to return it in different transforms. Since the number + # of supported transform is small and the probability that a given + # parameter will always be used in a specific transform, this should be + # an acceptable tradeoff. + value: Optional[Union[str, bytes, dict]] = None + key = (name, transform) + + # If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS + max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age) + + # if not force_fetch and self.has_not_expired_in_cache(key): + # return self.store[key].value + + try: + value = self._get(name, **sdk_options) + except Exception as exc: + raise UpdateSecretError(str(exc)) from exc + + if transform: + value = transform_value(key=name, value=value, transform=transform, raise_on_transform_error=True) + + # NOTE: don't cache None, as they might've been failed transforms and may be corrected + if value is not None: + self.store[key] = ExpirableValue(value, datetime.now() + timedelta(seconds=max_age)) + + return value + @staticmethod def _build_boto3_client( service_name: str, @@ -301,6 +364,9 @@ def _build_boto3_resource_client( return client + + + def get_transform_method(value: str, transform: TransformOptions = None) -> Callable[..., Any]: """ Determine the transform method diff --git a/aws_lambda_powertools/utilities/parameters/exceptions.py b/aws_lambda_powertools/utilities/parameters/exceptions.py index 1287568b463..c93928f0188 100644 --- a/aws_lambda_powertools/utilities/parameters/exceptions.py +++ b/aws_lambda_powertools/utilities/parameters/exceptions.py @@ -9,3 +9,7 @@ class GetParameterError(Exception): class TransformParameterError(Exception): """When a provider fails to transform a parameter value""" + + +class UpdateSecretError(Exception): + """When a provider fails an exception on updating a secret""" \ No newline at end of file diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index b11cb472012..85c63f10c72 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -113,6 +113,33 @@ def _get_multiple(self, path: str, **sdk_options) -> Dict[str, str]: """ raise NotImplementedError() + def _update(self, name: str, secret: Optional[str], secret_binary: Optional[str],**sdk_options) -> str: + """ + Modifies the details of a secret, including metadata and the secret value. + + Parameters + ---------- + name: str + Name of the parameter + sdk_options: dict, optional + Dictionary of options that will be passed to the Secrets Manager update_secret API call + + URLs: + https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager/client/update_secret.html + """ + + # Explicit arguments will take precedence over keyword arguments + sdk_options["SecretId"] = name + if secret: + sdk_options["SecretString"] = secret + if secret_binary: + sdk_options["SecretBinary"] = secret_binary + + update_secret = self.client.update_secret(**sdk_options) + + return update_secret["VersionId"] + + def get_secret( name: str, transform: Optional[str] = None, force_fetch: bool = False, max_age: Optional[int] = None, **sdk_options @@ -172,3 +199,60 @@ def get_secret( return DEFAULT_PROVIDERS["secrets"].get( name, max_age=max_age, transform=transform, force_fetch=force_fetch, **sdk_options ) + + +def update_secret( + name: str, transform: Optional[str] = None, force_fetch: bool = False, max_age: Optional[int] = None, **sdk_options +) -> Union[str, dict, bytes]: + """ + Retrieve a parameter value from AWS Secrets Manager + + Parameters + ---------- + name: str + Name of the parameter + transform: str, optional + Transforms the content from a JSON object ('json') or base64 binary string ('binary') + force_fetch: bool, optional + Force update even before a cached item has expired, defaults to False + max_age: int, optional + Maximum age of the cached value + sdk_options: dict, optional + Dictionary of options that will be passed to the get_secret_value call + + Raises + ------ + GetParameterError + When the parameter provider fails to retrieve a parameter value for + a given name. + TransformParameterError + When the parameter provider fails to transform a parameter value. + + Example + ------- + **Retrieves a secret*** + + >>> from aws_lambda_powertools.utilities.parameters import get_secret + >>> + >>> get_secret("my-secret") + + **Retrieves a secret and transforms using a JSON deserializer*** + + >>> from aws_lambda_powertools.utilities.parameters import get_secret + >>> + >>> get_secret("my-secret", transform="json") + + **Retrieves a secret and passes custom arguments to the SDK** + + >>> from aws_lambda_powertools.utilities.parameters import get_secret + >>> + >>> get_secret("my-secret", VersionId="f658cac0-98a5-41d9-b993-8a76a7799194") + """ + + # Only create the provider if this function is called at least once + if "secrets" not in DEFAULT_PROVIDERS: + DEFAULT_PROVIDERS["secrets"] = SecretsProvider() + + return DEFAULT_PROVIDERS["secrets"].update( + name, transform=transform, force_fetch=force_fetch, **sdk_options + ) From 55511185394033076e529b5296a2388c6136a2b4 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Tue, 1 Aug 2023 07:58:22 -0400 Subject: [PATCH 02/67] small tweaks --- aws_lambda_powertools/utilities/parameters/base.py | 1 - aws_lambda_powertools/utilities/parameters/secrets.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/base.py b/aws_lambda_powertools/utilities/parameters/base.py index 9efc86aba6f..aefbdff9027 100644 --- a/aws_lambda_powertools/utilities/parameters/base.py +++ b/aws_lambda_powertools/utilities/parameters/base.py @@ -231,7 +231,6 @@ def update( self, name: str, transform: TransformOptions = None, - # force_fetch: bool = False, **sdk_options, ) -> Optional[Union[str, dict, bytes]]: """ diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index 85c63f10c72..dc43a6af577 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -202,7 +202,7 @@ def get_secret( def update_secret( - name: str, transform: Optional[str] = None, force_fetch: bool = False, max_age: Optional[int] = None, **sdk_options + name: str, transform: Optional[str] = None, max_age: Optional[int] = None, **sdk_options ) -> Union[str, dict, bytes]: """ Retrieve a parameter value from AWS Secrets Manager @@ -254,5 +254,5 @@ def update_secret( DEFAULT_PROVIDERS["secrets"] = SecretsProvider() return DEFAULT_PROVIDERS["secrets"].update( - name, transform=transform, force_fetch=force_fetch, **sdk_options + name, max_age=max_age, transform=transform, **sdk_options ) From 6e0c7defc3d5da8164a89b272c8540d01863c702 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Sat, 2 Sep 2023 09:48:08 -0400 Subject: [PATCH 03/67] adding set param --- .../utilities/parameters/base.py | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/aws_lambda_powertools/utilities/parameters/base.py b/aws_lambda_powertools/utilities/parameters/base.py index aefbdff9027..9c943e1bb65 100644 --- a/aws_lambda_powertools/utilities/parameters/base.py +++ b/aws_lambda_powertools/utilities/parameters/base.py @@ -152,6 +152,75 @@ def _get(self, name: str, **sdk_options) -> Union[str, bytes]: """ raise NotImplementedError() + def set( + self, + name: str, + max_age: Optional[int] = None, + transform: TransformOptions = None, + **sdk_options, + ) -> Optional[Union[str, dict, bytes]]: + """ + Retrieve a parameter value or return the cached value + + Parameters + ---------- + name: str + Parameter name + max_age: int + Maximum age of the cached value + transform: str + Optional transformation of the parameter value. Supported values + are "json" for JSON strings and "binary" for base 64 encoded + values. + sdk_options: dict, optional + Arguments that will be passed directly to the underlying API call + + Raises + ------ + SetParameterError + When the parameter provider fails to retrieve a parameter value for + a given name. + TransformParameterError + When the parameter provider fails to transform a parameter value. + """ + + # If there are multiple calls to the same parameter but in a different + # transform, they will be stored multiple times. This allows us to + # optimize by transforming the data only once per retrieval, thus there + # is no need to transform cached values multiple times. However, this + # means that we need to make multiple calls to the underlying parameter + # store if we need to return it in different transforms. Since the number + # of supported transform is small and the probability that a given + # parameter will always be used in a specific transform, this should be + # an acceptable tradeoff. + value: Optional[Union[str, bytes, dict]] = None + key = self._build_cache_key(name=name, transform=transform) + + # If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS + max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age) + + try: + value = self._set(name, **sdk_options) + # Encapsulate all errors into a generic GetParameterError + except Exception as exc: + raise SetParameterError(str(exc)) from exc + + if transform: + value = transform_value(key=name, value=value, transform=transform, raise_on_transform_error=True) + + # NOTE: don't cache None, as they might've been failed transforms and may be corrected + if value is not None: + self.add_to_cache(key=key, value=value, max_age=max_age) + + return value + + @abstractmethod + def _set(self, name: str, **sdk_options) -> Union[str, bytes]: + """ + Set parameter value from the underlying parameter store + """ + raise NotImplementedError() + def get_multiple( self, path: str, From 330de3561de03d9af7b4b5bdc369a73d3197438e Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Sat, 2 Sep 2023 09:49:04 -0400 Subject: [PATCH 04/67] adding exception classes --- aws_lambda_powertools/utilities/parameters/base.py | 2 +- aws_lambda_powertools/utilities/parameters/exceptions.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/aws_lambda_powertools/utilities/parameters/base.py b/aws_lambda_powertools/utilities/parameters/base.py index 9c943e1bb65..20f96a16c0e 100644 --- a/aws_lambda_powertools/utilities/parameters/base.py +++ b/aws_lambda_powertools/utilities/parameters/base.py @@ -29,7 +29,7 @@ from aws_lambda_powertools.shared.functions import resolve_max_age from aws_lambda_powertools.utilities.parameters.types import TransformOptions -from .exceptions import GetParameterError, TransformParameterError, UpdateSecretError +from .exceptions import GetParameterError, SetParameterError, TransformParameterError, UpdateSecretError if TYPE_CHECKING: from mypy_boto3_appconfigdata import AppConfigDataClient diff --git a/aws_lambda_powertools/utilities/parameters/exceptions.py b/aws_lambda_powertools/utilities/parameters/exceptions.py index c93928f0188..898f18be5c6 100644 --- a/aws_lambda_powertools/utilities/parameters/exceptions.py +++ b/aws_lambda_powertools/utilities/parameters/exceptions.py @@ -10,6 +10,8 @@ class GetParameterError(Exception): class TransformParameterError(Exception): """When a provider fails to transform a parameter value""" +class SetParameterError(Exception): + """When a provider raises an exception on parameter retrieval""" class UpdateSecretError(Exception): """When a provider fails an exception on updating a secret""" \ No newline at end of file From 929ae7fc6e172ccf890bdb13b43d3c0c612c0bbf Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Sat, 2 Sep 2023 09:50:00 -0400 Subject: [PATCH 05/67] adding set parameter --- .../utilities/parameters/ssm.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index 26f225cfb28..21fabf4d6f6 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -184,6 +184,27 @@ def _get(self, name: str, decrypt: bool = False, **sdk_options) -> str: return self.client.get_parameter(**sdk_options)["Parameter"]["Value"] + def _set(self, name: str, parameter_type: str = "String", overwrite: bool = False, **sdk_options) -> str: + """ + Sets a parameter value from AWS Systems Manager Parameter Store + + Parameters + ---------- + name: str + Parameter name + decrypt: bool, optional + If the parameter value should be decrypted + sdk_options: dict, optional + Dictionary of options that will be passed to the Parameter Store put_parameter API call + """ + + # Explicit arguments will take precedence over keyword arguments + sdk_options["Name"] = name + sdk_options["Type"] = parameter_type + sdk_options["Overwrite"] = overwrite + + return self.client.put_parameter(**sdk_options)["Parameter"]["Value"] + def _get_multiple(self, path: str, decrypt: bool = False, recursive: bool = False, **sdk_options) -> Dict[str, str]: """ Retrieve multiple parameter values from AWS Systems Manager Parameter Store From 2ae911e4c2867b85a87504986748d6a743fba102 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Sat, 2 Sep 2023 17:00:55 -0400 Subject: [PATCH 06/67] fixing set --- .../utilities/parameters/base.py | 69 +----------------- .../utilities/parameters/ssm.py | 70 ++++++++++++++++++- 2 files changed, 70 insertions(+), 69 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/base.py b/aws_lambda_powertools/utilities/parameters/base.py index 20f96a16c0e..1a60f431916 100644 --- a/aws_lambda_powertools/utilities/parameters/base.py +++ b/aws_lambda_powertools/utilities/parameters/base.py @@ -152,72 +152,10 @@ def _get(self, name: str, **sdk_options) -> Union[str, bytes]: """ raise NotImplementedError() - def set( - self, - name: str, - max_age: Optional[int] = None, - transform: TransformOptions = None, - **sdk_options, - ) -> Optional[Union[str, dict, bytes]]: - """ - Retrieve a parameter value or return the cached value - - Parameters - ---------- - name: str - Parameter name - max_age: int - Maximum age of the cached value - transform: str - Optional transformation of the parameter value. Supported values - are "json" for JSON strings and "binary" for base 64 encoded - values. - sdk_options: dict, optional - Arguments that will be passed directly to the underlying API call - - Raises - ------ - SetParameterError - When the parameter provider fails to retrieve a parameter value for - a given name. - TransformParameterError - When the parameter provider fails to transform a parameter value. - """ - - # If there are multiple calls to the same parameter but in a different - # transform, they will be stored multiple times. This allows us to - # optimize by transforming the data only once per retrieval, thus there - # is no need to transform cached values multiple times. However, this - # means that we need to make multiple calls to the underlying parameter - # store if we need to return it in different transforms. Since the number - # of supported transform is small and the probability that a given - # parameter will always be used in a specific transform, this should be - # an acceptable tradeoff. - value: Optional[Union[str, bytes, dict]] = None - key = self._build_cache_key(name=name, transform=transform) - - # If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS - max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age) - - try: - value = self._set(name, **sdk_options) - # Encapsulate all errors into a generic GetParameterError - except Exception as exc: - raise SetParameterError(str(exc)) from exc - - if transform: - value = transform_value(key=name, value=value, transform=transform, raise_on_transform_error=True) - - # NOTE: don't cache None, as they might've been failed transforms and may be corrected - if value is not None: - self.add_to_cache(key=key, value=value, max_age=max_age) - - return value - @abstractmethod - def _set(self, name: str, **sdk_options) -> Union[str, bytes]: + def set(self, name: str, **sdk_options) -> Union[str, bytes]: """ - Set parameter value from the underlying parameter store + Retrieve parameter value from the underlying parameter store """ raise NotImplementedError() @@ -342,9 +280,6 @@ def update( # If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age) - # if not force_fetch and self.has_not_expired_in_cache(key): - # return self.store[key].value - try: value = self._get(name, **sdk_options) except Exception as exc: diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index 21fabf4d6f6..a3aef6bfc39 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -184,7 +184,7 @@ def _get(self, name: str, decrypt: bool = False, **sdk_options) -> str: return self.client.get_parameter(**sdk_options)["Parameter"]["Value"] - def _set(self, name: str, parameter_type: str = "String", overwrite: bool = False, **sdk_options) -> str: + def set(self, name: str, value: str, parameter_type: str = "String", overwrite: bool = False, **sdk_options) -> str: """ Sets a parameter value from AWS Systems Manager Parameter Store @@ -192,6 +192,8 @@ def _set(self, name: str, parameter_type: str = "String", overwrite: bool = Fals ---------- name: str Parameter name + value: str + Parameter value decrypt: bool, optional If the parameter value should be decrypted sdk_options: dict, optional @@ -200,10 +202,11 @@ def _set(self, name: str, parameter_type: str = "String", overwrite: bool = Fals # Explicit arguments will take precedence over keyword arguments sdk_options["Name"] = name + sdk_options["Value"] = value sdk_options["Type"] = parameter_type sdk_options["Overwrite"] = overwrite - return self.client.put_parameter(**sdk_options)["Parameter"]["Value"] + return self.client.put_parameter(**sdk_options)["Version"] def _get_multiple(self, path: str, decrypt: bool = False, recursive: bool = False, **sdk_options) -> Dict[str, str]: """ @@ -690,6 +693,69 @@ def get_parameters( ) +def set_parameter( + name: str, + transform: Optional[str] = None, + max_age: Optional[int] = None, + parameter_type: str = "String", + overwrite: bool = False, + **sdk_options, +) -> Union[str, dict, bytes]: + """ + Retrieve a parameter value from AWS Systems Manager (SSM) Parameter Store + + Parameters + ---------- + name: str + Name of the parameter + transform: str, optional + Transforms the content from a JSON object ('json') or base64 binary string ('binary') + max_age: int, optional + Maximum age of the cached value + parameter_type: str, optional + Type of the parameter. Allowed values are String, StringList, and SecureString + overwrite: bool, optional + If the parameter value should be overwritten + sdk_options: dict, optional + Dictionary of options that will be passed to the Parameter Store get_parameter API call + + Raises + ------ + GetParameterError + When the parameter provider fails to retrieve a parameter value for + a given name. + TransformParameterError + When the parameter provider fails to transform a parameter value. + + Example + ------- + **Sets a parameter value from Systems Manager Parameter Store** + + >>> from aws_lambda_powertools.utilities.parameters import set_parameter + >>> + >>> value = set_parameter("/my/parameter") + >>> + >>> print(value) + My parameter value + """ + + # Only create the provider if this function is called at least once + if "ssm" not in DEFAULT_PROVIDERS: + DEFAULT_PROVIDERS["ssm"] = SSMProvider() + + # If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS + max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age) + + # Add to `decrypt` sdk_options to we can have an explicit option for this + sdk_options["Name"] = name + sdk_options["Type"] = parameter_type + sdk_options["Overwrite"] = overwrite + + return DEFAULT_PROVIDERS["ssm"].set( + name, max_age=max_age, transform=transform, **sdk_options + ) + + @overload def get_parameters_by_name( parameters: Dict[str, Dict], From aedab55b0b372a708444042fa608039b66dff795 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Mon, 11 Sep 2023 19:19:34 -0400 Subject: [PATCH 07/67] few more updates --- .../utilities/parameters/base.py | 101 ++++++------------ .../utilities/parameters/exceptions.py | 3 - .../utilities/parameters/secrets.py | 81 ++++++++++---- 3 files changed, 91 insertions(+), 94 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/base.py b/aws_lambda_powertools/utilities/parameters/base.py index 1a60f431916..2e7a867a8ee 100644 --- a/aws_lambda_powertools/utilities/parameters/base.py +++ b/aws_lambda_powertools/utilities/parameters/base.py @@ -29,7 +29,7 @@ from aws_lambda_powertools.shared.functions import resolve_max_age from aws_lambda_powertools.utilities.parameters.types import TransformOptions -from .exceptions import GetParameterError, SetParameterError, TransformParameterError, UpdateSecretError +from .exceptions import GetParameterError, SetParameterError, TransformParameterError if TYPE_CHECKING: from mypy_boto3_appconfigdata import AppConfigDataClient @@ -66,16 +66,16 @@ class BaseProvider(ABC): Abstract Base Class for Parameter providers """ - store: Dict[Tuple[str, TransformOptions], ExpirableValue] + store: Dict[Tuple, ExpirableValue] def __init__(self): """ Initialize the base provider """ - self.store: Dict[Tuple[str, TransformOptions], ExpirableValue] = {} + self.store: Dict[Tuple, ExpirableValue] = {} - def has_not_expired_in_cache(self, key: Tuple[str, TransformOptions]) -> bool: + def has_not_expired_in_cache(self, key: Tuple) -> bool: return key in self.store and self.store[key].ttl >= datetime.now() def get( @@ -123,25 +123,26 @@ def get( # parameter will always be used in a specific transform, this should be # an acceptable tradeoff. value: Optional[Union[str, bytes, dict]] = None - key = (name, transform) + key = self._build_cache_key(name=name, transform=transform) # If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age) if not force_fetch and self.has_not_expired_in_cache(key): - return self.store[key].value + return self.fetch_from_cache(key) try: value = self._get(name, **sdk_options) + # Encapsulate all errors into a generic GetParameterError except Exception as exc: - raise GetParameterError(str(exc)) from exc + raise GetParameterError(str(exc)) if transform: value = transform_value(key=name, value=value, transform=transform, raise_on_transform_error=True) # NOTE: don't cache None, as they might've been failed transforms and may be corrected if value is not None: - self.store[key] = ExpirableValue(value, datetime.now() + timedelta(seconds=max_age)) + self.add_to_cache(key=key, value=value, max_age=max_age) return value @@ -153,9 +154,9 @@ def _get(self, name: str, **sdk_options) -> Union[str, bytes]: raise NotImplementedError() @abstractmethod - def set(self, name: str, **sdk_options) -> Union[str, bytes]: + def _set(self, name: str, **sdk_options) -> Union[str, bytes]: """ - Retrieve parameter value from the underlying parameter store + Sets a parameter value from the underlying parameter store """ raise NotImplementedError() @@ -197,13 +198,13 @@ def get_multiple( TransformParameterError When the parameter provider fails to transform a parameter value. """ - key = (path, transform) + key = self._build_cache_key(name=path, transform=transform, is_nested=True) # If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age) if not force_fetch and self.has_not_expired_in_cache(key): - return self.store[key].value # type: ignore # need to revisit entire typing here + return self.fetch_from_cache(key) try: values = self._get_multiple(path, **sdk_options) @@ -214,7 +215,7 @@ def get_multiple( if transform: values.update(transform_value(values, transform, raise_on_transform_error)) - self.store[key] = ExpirableValue(values, datetime.now() + timedelta(seconds=max_age)) + self.add_to_cache(key=key, value=values, max_age=max_age) return values @@ -228,71 +229,38 @@ def _get_multiple(self, path: str, **sdk_options) -> Dict[str, str]: def clear_cache(self): self.store.clear() - def add_to_cache(self, key: Tuple[str, TransformOptions], value: Any, max_age: int): + def fetch_from_cache(self, key: Tuple): + return self.store[key].value if key in self.store else {} + + def add_to_cache(self, key: Tuple, value: Any, max_age: int): if max_age <= 0: return self.store[key] = ExpirableValue(value, datetime.now() + timedelta(seconds=max_age)) - def update( + def _build_cache_key( self, name: str, transform: TransformOptions = None, - **sdk_options, - ) -> Optional[Union[str, dict, bytes]]: - """ - Modifies the details of a secret, including metadata and the secret value. + is_nested: bool = False, + ): + """Creates cache key for parameters Parameters ---------- - name: str - Parameter name - transform: str - Optional transformation of the parameter value. Supported values - are "json" for JSON strings and "binary" for base 64 encoded - values. - force_fetch: bool, optional - Force update even before a cached item has expired, defaults to False - sdk_options: dict, optional - Arguments that will be passed directly to the underlying API call + name : str + Name of parameter, secret or config + transform : TransformOptions, optional + Transform method used, by default None + is_nested : bool, optional + Whether it's a single parameter or multiple nested parameters, by default False - Raises - ------ - GetParameterError - When the parameter provider fails to retrieve a parameter value for - a given name. - TransformParameterError - When the parameter provider fails to transform a parameter value. + Returns + ------- + Tuple[str, TransformOptions, bool] + Cache key """ - - # If there are multiple calls to the same parameter but in a different - # transform, they will be stored multiple times. This allows us to - # optimize by transforming the data only once per retrieval, thus there - # is no need to transform cached values multiple times. However, this - # means that we need to make multiple calls to the underlying parameter - # store if we need to return it in different transforms. Since the number - # of supported transform is small and the probability that a given - # parameter will always be used in a specific transform, this should be - # an acceptable tradeoff. - value: Optional[Union[str, bytes, dict]] = None - key = (name, transform) - - # If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS - max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age) - - try: - value = self._get(name, **sdk_options) - except Exception as exc: - raise UpdateSecretError(str(exc)) from exc - - if transform: - value = transform_value(key=name, value=value, transform=transform, raise_on_transform_error=True) - - # NOTE: don't cache None, as they might've been failed transforms and may be corrected - if value is not None: - self.store[key] = ExpirableValue(value, datetime.now() + timedelta(seconds=max_age)) - - return value + return (name, transform, is_nested) @staticmethod def _build_boto3_client( @@ -367,9 +335,6 @@ def _build_boto3_resource_client( return client - - - def get_transform_method(value: str, transform: TransformOptions = None) -> Callable[..., Any]: """ Determine the transform method diff --git a/aws_lambda_powertools/utilities/parameters/exceptions.py b/aws_lambda_powertools/utilities/parameters/exceptions.py index 898f18be5c6..ffcf3b7dd55 100644 --- a/aws_lambda_powertools/utilities/parameters/exceptions.py +++ b/aws_lambda_powertools/utilities/parameters/exceptions.py @@ -12,6 +12,3 @@ class TransformParameterError(Exception): class SetParameterError(Exception): """When a provider raises an exception on parameter retrieval""" - -class UpdateSecretError(Exception): - """When a provider fails an exception on updating a secret""" \ No newline at end of file diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index dc43a6af577..4efc4b28872 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -4,6 +4,7 @@ import os +import json from typing import TYPE_CHECKING, Any, Dict, Optional, Union import boto3 @@ -113,32 +114,49 @@ def _get_multiple(self, path: str, **sdk_options) -> Dict[str, str]: """ raise NotImplementedError() - def _update(self, name: str, secret: Optional[str], secret_binary: Optional[str],**sdk_options) -> str: + def set_secret( + self, + name: str, + secret: Optional[str], + secret_binary: Optional[str], + idempotency_id: Optional[str], + version_stages: Optional[list[str]], + **sdk_options + ) -> str: """ Modifies the details of a secret, including metadata and the secret value. Parameters ---------- name: str - Name of the parameter + The ARN or name of the secret to add a new version to. sdk_options: dict, optional Dictionary of options that will be passed to the Secrets Manager update_secret API call URLs: - https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager/client/update_secret.html + https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager/client/put_secret_value.html """ + if isinstance(secret, dict): + secret = json.dumps(secret) + + if not isinstance(secret_binary, bytes): + secret_binary = secret_binary.encode("utf-8") + # Explicit arguments will take precedence over keyword arguments sdk_options["SecretId"] = name if secret: sdk_options["SecretString"] = secret if secret_binary: sdk_options["SecretBinary"] = secret_binary + if version_stages: + sdk_options["VersionStages"] = version_stages + if idempotency_id: + sdk_options["ClientRequestToken"] = idempotency_id - update_secret = self.client.update_secret(**sdk_options) - - return update_secret["VersionId"] + put_secret = self.client.put_secret_value(**sdk_options) + return put_secret["VersionId"] def get_secret( @@ -201,8 +219,15 @@ def get_secret( ) -def update_secret( - name: str, transform: Optional[str] = None, max_age: Optional[int] = None, **sdk_options +def set_secret( + name: str, + secret: Optional[Union[str, dict]] = None, + secret_binary: Optional[bytes] = None, + idempotency_id: Optional[str] = None, + version_stages: Optional[list[str]] = None, + max_age: Optional[int] = None, + transform: Optional[str] = None, + **sdk_options ) -> Union[str, dict, bytes]: """ Retrieve a parameter value from AWS Secrets Manager @@ -211,18 +236,18 @@ def update_secret( ---------- name: str Name of the parameter - transform: str, optional - Transforms the content from a JSON object ('json') or base64 binary string ('binary') - force_fetch: bool, optional - Force update even before a cached item has expired, defaults to False max_age: int, optional Maximum age of the cached value + idempotency_token: str, optional + Idempotency token to use for the request to prevent the accidental + creation of duplicate versions if there are failures and retries + during the Lambda rotation function processing. sdk_options: dict, optional Dictionary of options that will be passed to the get_secret_value call Raises ------ - GetParameterError + SetParameterError When the parameter provider fails to retrieve a parameter value for a given name. TransformParameterError @@ -230,29 +255,39 @@ def update_secret( Example ------- - **Retrieves a secret*** + **Sets a secret*** - >>> from aws_lambda_powertools.utilities.parameters import get_secret + >>> from aws_lambda_powertools.utilities.parameters import set_secret >>> - >>> get_secret("my-secret") + >>> set_secret(name="llamas-are-awesome", secret="supers3cr3tllam@passw0rd") - **Retrieves a secret and transforms using a JSON deserializer*** + **Set a secret and transforms using a JSON deserializer*** >>> from aws_lambda_powertools.utilities.parameters import get_secret >>> >>> get_secret("my-secret", transform="json") - **Retrieves a secret and passes custom arguments to the SDK** + **Retrieves a secret and set an idempotency_id** - >>> from aws_lambda_powertools.utilities.parameters import get_secret + >>> from aws_lambda_powertools.utilities.parameters import set_secret >>> - >>> get_secret("my-secret", VersionId="f658cac0-98a5-41d9-b993-8a76a7799194") + >>> set_secret("my-secret", idempotency_id="f658cac0-98a5-41d9-b993-8a76a7799194") """ + # If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS + max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age) + # Only create the provider if this function is called at least once if "secrets" not in DEFAULT_PROVIDERS: DEFAULT_PROVIDERS["secrets"] = SecretsProvider() - return DEFAULT_PROVIDERS["secrets"].update( - name, max_age=max_age, transform=transform, **sdk_options - ) + if secret and secret_binary: + raise ValueError("secret and secret_binary are mutually exclusive") + elif secret: + return DEFAULT_PROVIDERS["secrets"].set_secret( + name, secret=secret, idempotency_id=idempotency_id, version_stages=version_stages, max_age=max_age, **sdk_options + ) + elif secret_binary: + return DEFAULT_PROVIDERS["secrets"].set_secret( + name, secret_binary=secret_binary, idempotency_id=idempotency_id, version_stages=version_stages, max_age=max_age, **sdk_options + ) From e154a5dde2730f4f59bf6ed1db97abcd739a4e83 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Mon, 11 Sep 2023 19:26:38 -0400 Subject: [PATCH 08/67] missing value --- aws_lambda_powertools/utilities/parameters/ssm.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index acbafd81f17..bdb9711d36b 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -728,6 +728,7 @@ def get_parameters( def set_parameter( name: str, + value: str, transform: Optional[str] = None, max_age: Optional[int] = None, parameter_type: str = "String", @@ -781,6 +782,7 @@ def set_parameter( # Add to `decrypt` sdk_options to we can have an explicit option for this sdk_options["Name"] = name + sdk_options["Value"] = value sdk_options["Type"] = parameter_type sdk_options["Overwrite"] = overwrite From 805f3d86006bd65cd6c9102384602ee0463649a5 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Mon, 11 Sep 2023 19:33:36 -0400 Subject: [PATCH 09/67] adding documentation --- .../utilities/parameters/secrets.py | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index cc2f4a92699..681cdaf03e8 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -237,7 +237,6 @@ def set_secret( idempotency_id: Optional[str] = None, version_stages: Optional[list[str]] = None, max_age: Optional[int] = None, - transform: Optional[str] = None, **sdk_options ) -> Union[str, dict, bytes]: """ @@ -247,22 +246,26 @@ def set_secret( ---------- name: str Name of the parameter - max_age: int, optional - Maximum age of the cached value + secret: str, optional + Secret value to set + secret_binary: bytes, optional + Secret binary value to set idempotency_token: str, optional Idempotency token to use for the request to prevent the accidental creation of duplicate versions if there are failures and retries during the Lambda rotation function processing. + version_stages: list[str], optional + A list of staging labels that are attached to this version of the secret. + max_age: int, optional + Maximum age of the cached value sdk_options: dict, optional Dictionary of options that will be passed to the get_secret_value call Raises ------ SetParameterError - When the parameter provider fails to retrieve a parameter value for + When the secrets provider fails to set a secret value or secret binary for a given name. - TransformParameterError - When the parameter provider fails to transform a parameter value. Example ------- @@ -272,17 +275,11 @@ def set_secret( >>> >>> set_secret(name="llamas-are-awesome", secret="supers3cr3tllam@passw0rd") - **Set a secret and transforms using a JSON deserializer*** - - >>> from aws_lambda_powertools.utilities.parameters import get_secret - >>> - >>> get_secret("my-secret", transform="json") - - **Retrieves a secret and set an idempotency_id** + **Sets a secret and includes an idempotency_id** >>> from aws_lambda_powertools.utilities.parameters import set_secret >>> - >>> set_secret("my-secret", idempotency_id="f658cac0-98a5-41d9-b993-8a76a7799194") + >>> set_secret("my-secret", secret="supers3cr3tllam@passw0rd", idempotency_id="f658cac0-98a5-41d9-b993-8a76a7799194") """ # If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS From 12b885035af0d89edbcd0c52f86a60089b912cb0 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Mon, 11 Sep 2023 19:40:49 -0400 Subject: [PATCH 10/67] one more update --- aws_lambda_powertools/utilities/parameters/secrets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index 681cdaf03e8..6f88ce3114b 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -273,13 +273,13 @@ def set_secret( >>> from aws_lambda_powertools.utilities.parameters import set_secret >>> - >>> set_secret(name="llamas-are-awesome", secret="supers3cr3tllam@passw0rd") + >>> set_secret(name="llamas-are-awesome", secret={"password": "supers3cr3tllam@passw0rd"}) **Sets a secret and includes an idempotency_id** >>> from aws_lambda_powertools.utilities.parameters import set_secret >>> - >>> set_secret("my-secret", secret="supers3cr3tllam@passw0rd", idempotency_id="f658cac0-98a5-41d9-b993-8a76a7799194") + >>> set_secret("my-secret", secret='{"password": "supers3cr3tllam@passw0rd"}', idempotency_id="f658cac0-98a5-41d9-b993-8a76a7799194") """ # If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS From f9323ea1035b4da71eb3b9316693d09faf148ca0 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Thu, 14 Sep 2023 08:19:47 -0400 Subject: [PATCH 11/67] question about transform yet --- .../utilities/parameters/base.py | 64 ++++++++++++++++++ .../utilities/parameters/ssm.py | 67 +++++++++++++++++-- 2 files changed, 125 insertions(+), 6 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/base.py b/aws_lambda_powertools/utilities/parameters/base.py index 2e7a867a8ee..9891cbfc9ee 100644 --- a/aws_lambda_powertools/utilities/parameters/base.py +++ b/aws_lambda_powertools/utilities/parameters/base.py @@ -146,6 +146,70 @@ def get( return value + def set( + self, + name: str, + max_age: Optional[int] = None, + transform: TransformOptions = None, + **sdk_options, + ) -> Optional[Union[str, dict, bytes]]: + """ + Retrieve a parameter value or return the cached value + + Parameters + ---------- + name: str + Parameter name + max_age: int + Maximum age of the cached value + transform: str + Optional transformation of the parameter value. Supported values + are "json" for JSON strings and "binary" for base 64 encoded + values. + force_fetch: bool, optional + Force update even before a cached item has expired, defaults to False + sdk_options: dict, optional + Arguments that will be passed directly to the underlying API call + + Raises + ------ + GetParameterError + When the parameter provider fails to retrieve a parameter value for + a given name. + TransformParameterError + When the parameter provider fails to transform a parameter value. + """ + + # If there are multiple calls to the same parameter but in a different + # transform, they will be stored multiple times. This allows us to + # optimize by transforming the data only once per retrieval, thus there + # is no need to transform cached values multiple times. However, this + # means that we need to make multiple calls to the underlying parameter + # store if we need to return it in different transforms. Since the number + # of supported transform is small and the probability that a given + # parameter will always be used in a specific transform, this should be + # an acceptable tradeoff. + value: Optional[Union[str, bytes, dict]] = None + key = self._build_cache_key(name=name, transform=transform) + + # If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS + max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age) + + try: + value = self._set(name, **sdk_options) + # Encapsulate all errors into a generic SetParameterError + except Exception as exc: + raise SetParameterError(str(exc)) from exc + + if transform: + value = transform_value(key=name, value=value, transform=transform, raise_on_transform_error=True) + + # NOTE: don't cache None, as they might've been failed transforms and may be corrected + if value is not None: + self.add_to_cache(key=key, value=value, max_age=max_age) + + return value + @abstractmethod def _get(self, name: str, **sdk_options) -> Union[str, bytes]: """ diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index bdb9711d36b..366cdaf39a1 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -188,7 +188,64 @@ def _get(self, name: str, decrypt: bool = False, **sdk_options) -> str: return self.client.get_parameter(**sdk_options)["Parameter"]["Value"] - def set(self, name: str, value: str, parameter_type: str = "String", overwrite: bool = False, **sdk_options) -> str: + def set( + self, + name: str, + value: str, + type: Optional[str] = "String", + overwrite: bool = False, + max_age: Optional[int] = None, + transform: TransformOptions = None, + **sdk_options, + ) -> Optional[Union[str, dict, bytes]]: + """ + Retrieve a parameter value or return the cached value + + Parameters + ---------- + name: str + Parameter name + value: str + Parameter value + type: str, optional + Parameter type (Allowed: 'String','StringList','SecureString') + Items in a StringList must be separated by a comma (,). + max_age: int, optional + Maximum age of the cached value + transform: str + Optional transformation of the parameter value. Supported values + are "json" for JSON strings and "binary" for base 64 encoded + values. + sdk_options: dict, optional + Arguments that will be passed directly to the underlying API call + + Raises + ------ + GetParameterError + When the parameter provider fails to retrieve a parameter value for + a given name. + TransformParameterError + When the parameter provider fails to transform a parameter value. + """ + + # If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS + max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age) + + sdk_options["Name"] = name + sdk_options["Value"] = value + sdk_options["Type"] = type + sdk_options["Overwrite"] = overwrite + + return super().set(name, max_age, transform, **sdk_options) + + def _set( + self, + name: str, + value: str, + type: str = "String", + overwrite: bool = False, + **sdk_options + ) -> str: """ Sets a parameter value from AWS Systems Manager Parameter Store @@ -198,8 +255,6 @@ def set(self, name: str, value: str, parameter_type: str = "String", overwrite: Parameter name value: str Parameter value - decrypt: bool, optional - If the parameter value should be decrypted sdk_options: dict, optional Dictionary of options that will be passed to the Parameter Store put_parameter API call """ @@ -207,7 +262,7 @@ def set(self, name: str, value: str, parameter_type: str = "String", overwrite: # Explicit arguments will take precedence over keyword arguments sdk_options["Name"] = name sdk_options["Value"] = value - sdk_options["Type"] = parameter_type + sdk_options["Type"] = type sdk_options["Overwrite"] = overwrite return self.client.put_parameter(**sdk_options)["Version"] @@ -731,7 +786,7 @@ def set_parameter( value: str, transform: Optional[str] = None, max_age: Optional[int] = None, - parameter_type: str = "String", + type: str = "String", overwrite: bool = False, **sdk_options, ) -> Union[str, dict, bytes]: @@ -783,7 +838,7 @@ def set_parameter( # Add to `decrypt` sdk_options to we can have an explicit option for this sdk_options["Name"] = name sdk_options["Value"] = value - sdk_options["Type"] = parameter_type + sdk_options["Type"] = type sdk_options["Overwrite"] = overwrite return DEFAULT_PROVIDERS["ssm"].set( From a0b32b8cbc3f3c909350d623aff9a185ab2dada5 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Fri, 15 Sep 2023 08:25:56 -0400 Subject: [PATCH 12/67] remove optional transform --- aws_lambda_powertools/utilities/parameters/ssm.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index 366cdaf39a1..4deb2844f2d 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -195,7 +195,6 @@ def set( type: Optional[str] = "String", overwrite: bool = False, max_age: Optional[int] = None, - transform: TransformOptions = None, **sdk_options, ) -> Optional[Union[str, dict, bytes]]: """ @@ -236,7 +235,7 @@ def set( sdk_options["Type"] = type sdk_options["Overwrite"] = overwrite - return super().set(name, max_age, transform, **sdk_options) + return super().set(name, max_age, **sdk_options) def _set( self, @@ -784,7 +783,6 @@ def get_parameters( def set_parameter( name: str, value: str, - transform: Optional[str] = None, max_age: Optional[int] = None, type: str = "String", overwrite: bool = False, @@ -797,8 +795,6 @@ def set_parameter( ---------- name: str Name of the parameter - transform: str, optional - Transforms the content from a JSON object ('json') or base64 binary string ('binary') max_age: int, optional Maximum age of the cached value parameter_type: str, optional @@ -842,7 +838,7 @@ def set_parameter( sdk_options["Overwrite"] = overwrite return DEFAULT_PROVIDERS["ssm"].set( - name, max_age=max_age, transform=transform, **sdk_options + name, max_age=max_age, **sdk_options ) From ab6065e2e44bc6f739c655b61cd9b05c9985b9a6 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Sat, 16 Sep 2023 07:19:38 -0400 Subject: [PATCH 13/67] updates --- .../utilities/parameters/base.py | 64 -------------- .../utilities/parameters/ssm.py | 83 ++++++++----------- 2 files changed, 36 insertions(+), 111 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/base.py b/aws_lambda_powertools/utilities/parameters/base.py index 9891cbfc9ee..2e7a867a8ee 100644 --- a/aws_lambda_powertools/utilities/parameters/base.py +++ b/aws_lambda_powertools/utilities/parameters/base.py @@ -146,70 +146,6 @@ def get( return value - def set( - self, - name: str, - max_age: Optional[int] = None, - transform: TransformOptions = None, - **sdk_options, - ) -> Optional[Union[str, dict, bytes]]: - """ - Retrieve a parameter value or return the cached value - - Parameters - ---------- - name: str - Parameter name - max_age: int - Maximum age of the cached value - transform: str - Optional transformation of the parameter value. Supported values - are "json" for JSON strings and "binary" for base 64 encoded - values. - force_fetch: bool, optional - Force update even before a cached item has expired, defaults to False - sdk_options: dict, optional - Arguments that will be passed directly to the underlying API call - - Raises - ------ - GetParameterError - When the parameter provider fails to retrieve a parameter value for - a given name. - TransformParameterError - When the parameter provider fails to transform a parameter value. - """ - - # If there are multiple calls to the same parameter but in a different - # transform, they will be stored multiple times. This allows us to - # optimize by transforming the data only once per retrieval, thus there - # is no need to transform cached values multiple times. However, this - # means that we need to make multiple calls to the underlying parameter - # store if we need to return it in different transforms. Since the number - # of supported transform is small and the probability that a given - # parameter will always be used in a specific transform, this should be - # an acceptable tradeoff. - value: Optional[Union[str, bytes, dict]] = None - key = self._build_cache_key(name=name, transform=transform) - - # If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS - max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age) - - try: - value = self._set(name, **sdk_options) - # Encapsulate all errors into a generic SetParameterError - except Exception as exc: - raise SetParameterError(str(exc)) from exc - - if transform: - value = transform_value(key=name, value=value, transform=transform, raise_on_transform_error=True) - - # NOTE: don't cache None, as they might've been failed transforms and may be corrected - if value is not None: - self.add_to_cache(key=key, value=value, max_age=max_age) - - return value - @abstractmethod def _get(self, name: str, **sdk_options) -> Union[str, bytes]: """ diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index 4deb2844f2d..dc641af3e99 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -18,7 +18,7 @@ ) from .base import DEFAULT_MAX_AGE_SECS, DEFAULT_PROVIDERS, BaseProvider, transform_value -from .exceptions import GetParameterError +from .exceptions import GetParameterError, SetParameterError from .types import TransformOptions if TYPE_CHECKING: @@ -192,11 +192,15 @@ def set( self, name: str, value: str, + *, # force keyword arguments type: Optional[str] = "String", overwrite: bool = False, - max_age: Optional[int] = None, + tier: Optional[Literal["Standard", "Advanced", "Intelligent-Tiering"]] = "Standard", + description: Optional[str] = None, + kms_key_id: Optional[str] = None, + transform: Optional[str] = None, **sdk_options, - ) -> Optional[Union[str, dict, bytes]]: + ) -> str: """ Retrieve a parameter value or return the cached value @@ -227,44 +231,26 @@ def set( When the parameter provider fails to transform a parameter value. """ - # If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS - max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age) - sdk_options["Name"] = name sdk_options["Value"] = value sdk_options["Type"] = type sdk_options["Overwrite"] = overwrite + sdk_options["Tier"] = tier - return super().set(name, max_age, **sdk_options) - - def _set( - self, - name: str, - value: str, - type: str = "String", - overwrite: bool = False, - **sdk_options - ) -> str: - """ - Sets a parameter value from AWS Systems Manager Parameter Store + if description: + sdk_options["Description"] = description + if kms_key_id: + sdk_options["KeyId"] = kms_key_id - Parameters - ---------- - name: str - Parameter name - value: str - Parameter value - sdk_options: dict, optional - Dictionary of options that will be passed to the Parameter Store put_parameter API call - """ + try: + value = self.client.put_parameter(**sdk_options)["Version"] + except Exception as exc: + raise SetParameterError(str(exc)) from exc - # Explicit arguments will take precedence over keyword arguments - sdk_options["Name"] = name - sdk_options["Value"] = value - sdk_options["Type"] = type - sdk_options["Overwrite"] = overwrite + if transform: + value = transform_value(key=name, value=value, transform=transform, raise_on_transform_error=True) - return self.client.put_parameter(**sdk_options)["Version"] + return value def _get_multiple(self, path: str, decrypt: bool = False, recursive: bool = False, **sdk_options) -> Dict[str, str]: """ @@ -783,11 +769,15 @@ def get_parameters( def set_parameter( name: str, value: str, - max_age: Optional[int] = None, - type: str = "String", + *, # force keyword arguments + type: Optional[Literal["String", "StringList", "SecureString"]] = "String", overwrite: bool = False, + tier: Optional[Literal["Standard", "Advanced", "Intelligent-Tiering"]] = "Standard", + description: Optional[str] = None, + kms_key_id: Optional[str] = None, + transform: Optional[str] = None, **sdk_options, -) -> Union[str, dict, bytes]: +) -> str: """ Retrieve a parameter value from AWS Systems Manager (SSM) Parameter Store @@ -806,7 +796,7 @@ def set_parameter( Raises ------ - GetParameterError + SetParameterError When the parameter provider fails to retrieve a parameter value for a given name. TransformParameterError @@ -828,17 +818,16 @@ def set_parameter( if "ssm" not in DEFAULT_PROVIDERS: DEFAULT_PROVIDERS["ssm"] = SSMProvider() - # If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS - max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age) - - # Add to `decrypt` sdk_options to we can have an explicit option for this - sdk_options["Name"] = name - sdk_options["Value"] = value - sdk_options["Type"] = type - sdk_options["Overwrite"] = overwrite - return DEFAULT_PROVIDERS["ssm"].set( - name, max_age=max_age, **sdk_options + name, + value, + type=type, + overwrite=overwrite, + tier=tier, + description=description, + kms_key_id=kms_key_id, + transform=transform, + **sdk_options ) From 6d2012c6e8a32b19793b525e75728a8498c68d6f Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Sat, 16 Sep 2023 07:57:54 -0400 Subject: [PATCH 14/67] updating put secret value --- .../utilities/parameters/exceptions.py | 3 +- .../utilities/parameters/secrets.py | 39 ++++++++-------- .../utilities/parameters/ssm.py | 45 +++++++++++-------- 3 files changed, 48 insertions(+), 39 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/exceptions.py b/aws_lambda_powertools/utilities/parameters/exceptions.py index ffcf3b7dd55..eaaaed5af89 100644 --- a/aws_lambda_powertools/utilities/parameters/exceptions.py +++ b/aws_lambda_powertools/utilities/parameters/exceptions.py @@ -6,9 +6,8 @@ class GetParameterError(Exception): """When a provider raises an exception on parameter retrieval""" - class TransformParameterError(Exception): """When a provider fails to transform a parameter value""" class SetParameterError(Exception): - """When a provider raises an exception on parameter retrieval""" + """When a provider raises an exception on setting a parameter""" diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index 6f88ce3114b..731081525f0 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -17,6 +17,7 @@ from aws_lambda_powertools.shared.functions import resolve_max_age from .base import DEFAULT_MAX_AGE_SECS, DEFAULT_PROVIDERS, BaseProvider +from .exceptions import SetParameterError class SecretsProvider(BaseProvider): @@ -119,6 +120,7 @@ def _get_multiple(self, path: str, **sdk_options) -> Dict[str, str]: def set_secret( self, + *, # force keyword arguments name: str, secret: Optional[str], secret_binary: Optional[str], @@ -133,6 +135,16 @@ def set_secret( ---------- name: str The ARN or name of the secret to add a new version to. + secret: str, optional + Specifies text data that you want to encrypt and store in this new version of the secret. + secret_binary: bytes, optional + Specifies binary data that you want to encrypt and store in this new version of the secret. + idempotency_id: str, optional + Idempotency token to use for the request to prevent the accidental + creation of duplicate versions if there are failures and retries + during the Lambda rotation function processing. + version_stages: list[str], optional + Specifies a list of staging labels that are attached to this version of the secret. sdk_options: dict, optional Dictionary of options that will be passed to the Secrets Manager update_secret API call @@ -157,7 +169,10 @@ def set_secret( if idempotency_id: sdk_options["ClientRequestToken"] = idempotency_id - put_secret = self.client.put_secret_value(**sdk_options) + try: + put_secret = self.client.put_secret_value(**sdk_options)["VersionId"] + except Exception as exc: + raise SetParameterError(str(exc)) from exc return put_secret["VersionId"] @@ -231,14 +246,14 @@ def get_secret( def set_secret( + *, # force keyword arguments name: str, secret: Optional[Union[str, dict]] = None, secret_binary: Optional[bytes] = None, idempotency_id: Optional[str] = None, version_stages: Optional[list[str]] = None, - max_age: Optional[int] = None, **sdk_options -) -> Union[str, dict, bytes]: +) -> str: """ Retrieve a parameter value from AWS Secrets Manager @@ -256,8 +271,6 @@ def set_secret( during the Lambda rotation function processing. version_stages: list[str], optional A list of staging labels that are attached to this version of the secret. - max_age: int, optional - Maximum age of the cached value sdk_options: dict, optional Dictionary of options that will be passed to the get_secret_value call @@ -282,20 +295,10 @@ def set_secret( >>> set_secret("my-secret", secret='{"password": "supers3cr3tllam@passw0rd"}', idempotency_id="f658cac0-98a5-41d9-b993-8a76a7799194") """ - # If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS - max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age) - # Only create the provider if this function is called at least once if "secrets" not in DEFAULT_PROVIDERS: DEFAULT_PROVIDERS["secrets"] = SecretsProvider() - if secret and secret_binary: - raise ValueError("secret and secret_binary are mutually exclusive") - elif secret: - return DEFAULT_PROVIDERS["secrets"].set_secret( - name, secret=secret, idempotency_id=idempotency_id, version_stages=version_stages, max_age=max_age, **sdk_options - ) - elif secret_binary: - return DEFAULT_PROVIDERS["secrets"].set_secret( - name, secret_binary=secret_binary, idempotency_id=idempotency_id, version_stages=version_stages, max_age=max_age, **sdk_options - ) + return DEFAULT_PROVIDERS["secrets"].set_secret( + name=name, secret=secret, secret_binary=secret_binary, idempotency_id=idempotency_id, version_stages=version_stages, **sdk_options + ) diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index dc641af3e99..40a9195e2b4 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -207,25 +207,26 @@ def set( Parameters ---------- name: str - Parameter name - value: str - Parameter value + Name of the parameter type: str, optional - Parameter type (Allowed: 'String','StringList','SecureString') - Items in a StringList must be separated by a comma (,). - max_age: int, optional - Maximum age of the cached value - transform: str - Optional transformation of the parameter value. Supported values - are "json" for JSON strings and "binary" for base 64 encoded - values. + Type of the parameter. Allowed values are String, StringList, and SecureString + overwrite: bool, optional + If the parameter value should be overwritten, False by default + tier: str, optional + The parameter tier to use. Allowed values are Standard, Advanced, and Intelligent-Tiering + description: str, optional + The description of the parameter + kms_key_id: str, optional + The KMS key id to use to encrypt the parameter + transform: str, optional + Transforms the content from a JSON object ('json') or base64 binary string ('binary') sdk_options: dict, optional - Arguments that will be passed directly to the underlying API call + Dictionary of options that will be passed to the Parameter Store get_parameter API call Raises ------ - GetParameterError - When the parameter provider fails to retrieve a parameter value for + SetParameterError + When the parameter provider fails to set a parameter value for a given name. TransformParameterError When the parameter provider fails to transform a parameter value. @@ -785,12 +786,18 @@ def set_parameter( ---------- name: str Name of the parameter - max_age: int, optional - Maximum age of the cached value - parameter_type: str, optional + type: str, optional Type of the parameter. Allowed values are String, StringList, and SecureString overwrite: bool, optional - If the parameter value should be overwritten + If the parameter value should be overwritten, False by default + tier: str, optional + The parameter tier to use. Allowed values are Standard, Advanced, and Intelligent-Tiering + description: str, optional + The description of the parameter + kms_key_id: str, optional + The KMS key id to use to encrypt the parameter + transform: str, optional + Transforms the content from a JSON object ('json') or base64 binary string ('binary') sdk_options: dict, optional Dictionary of options that will be passed to the Parameter Store get_parameter API call @@ -808,7 +815,7 @@ def set_parameter( >>> from aws_lambda_powertools.utilities.parameters import set_parameter >>> - >>> value = set_parameter("/my/parameter") + >>> value = set_parameter("/my/parameter", "My parameter value", description="My parameter description") >>> >>> print(value) My parameter value From c6a1088159f07bd03cfc9531fea62979b875feb5 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Sat, 16 Sep 2023 08:03:40 -0400 Subject: [PATCH 15/67] fixing value on return secret --- aws_lambda_powertools/utilities/parameters/secrets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index 731081525f0..707aac20a24 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -170,11 +170,11 @@ def set_secret( sdk_options["ClientRequestToken"] = idempotency_id try: - put_secret = self.client.put_secret_value(**sdk_options)["VersionId"] + value = self.client.put_secret_value(**sdk_options)["VersionId"] except Exception as exc: raise SetParameterError(str(exc)) from exc - return put_secret["VersionId"] + return value def get_secret( From 609dbc93c79c3541bd0e4b82a8fbc6fac88ffaca Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Sat, 16 Sep 2023 14:26:44 -0400 Subject: [PATCH 16/67] cleaning up naming and examples --- .../utilities/parameters/__init__.py | 6 ++-- .../utilities/parameters/secrets.py | 23 ++++++++++--- .../utilities/parameters/ssm.py | 32 ++++++++++++------- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/__init__.py b/aws_lambda_powertools/utilities/parameters/__init__.py index 9fcaa4fa701..f07787769e7 100644 --- a/aws_lambda_powertools/utilities/parameters/__init__.py +++ b/aws_lambda_powertools/utilities/parameters/__init__.py @@ -8,8 +8,8 @@ from .base import BaseProvider, clear_caches from .dynamodb import DynamoDBProvider from .exceptions import GetParameterError, TransformParameterError -from .secrets import SecretsProvider, get_secret -from .ssm import SSMProvider, get_parameter, get_parameters, get_parameters_by_name +from .secrets import SecretsProvider, get_secret, set_secret +from .ssm import SSMProvider, get_parameter, set_parameter, get_parameters, get_parameters_by_name __all__ = [ "AppConfigProvider", @@ -21,8 +21,10 @@ "TransformParameterError", "get_app_config", "get_parameter", + "set_parameter", "get_parameters", "get_parameters_by_name", "get_secret", + "set_secret", "clear_caches", ] diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index 707aac20a24..e64de578c62 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -148,8 +148,13 @@ def set_secret( sdk_options: dict, optional Dictionary of options that will be passed to the Secrets Manager update_secret API call + Returns: + ------- + Version ID of the newly created version of the secret. + URLs: - https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager/client/put_secret_value.html + ------- + https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager/client/put_secret_value.html """ if isinstance(secret, dict): @@ -280,19 +285,27 @@ def set_secret( When the secrets provider fails to set a secret value or secret binary for a given name. + Returns: + ------- + Version ID of the newly created version of the secret. + + URLs: + ------- + https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager/client/put_secret_value.html + Example ------- **Sets a secret*** - >>> from aws_lambda_powertools.utilities.parameters import set_secret + >>> from aws_lambda_powertools.utilities import parameters >>> - >>> set_secret(name="llamas-are-awesome", secret={"password": "supers3cr3tllam@passw0rd"}) + >>> parameters.set_secret(name="llamas-are-awesome", secret={"password": "supers3cr3tllam@passw0rd"}) **Sets a secret and includes an idempotency_id** - >>> from aws_lambda_powertools.utilities.parameters import set_secret + >>> from aws_lambda_powertools.utilities import parameters >>> - >>> set_secret("my-secret", secret='{"password": "supers3cr3tllam@passw0rd"}', idempotency_id="f658cac0-98a5-41d9-b993-8a76a7799194") + >>> parameters.set_secret("my-secret", secret='{"password": "supers3cr3tllam@passw0rd"}', idempotency_id="f658cac0-98a5-41d9-b993-8a76a7799194") """ # Only create the provider if this function is called at least once diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index 40a9195e2b4..5b0ba076f2e 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -190,7 +190,7 @@ def _get(self, name: str, decrypt: bool = False, **sdk_options) -> str: def set( self, - name: str, + path: str, value: str, *, # force keyword arguments type: Optional[str] = "String", @@ -206,8 +206,8 @@ def set( Parameters ---------- - name: str - Name of the parameter + path: str + The fully qualified name includes the complete hierarchy of the parameter path and name. type: str, optional Type of the parameter. Allowed values are String, StringList, and SecureString overwrite: bool, optional @@ -232,7 +232,7 @@ def set( When the parameter provider fails to transform a parameter value. """ - sdk_options["Name"] = name + sdk_options["Name"] = path sdk_options["Value"] = value sdk_options["Type"] = type sdk_options["Overwrite"] = overwrite @@ -249,7 +249,7 @@ def set( raise SetParameterError(str(exc)) from exc if transform: - value = transform_value(key=name, value=value, transform=transform, raise_on_transform_error=True) + value = transform_value(key=path, value=value, transform=transform, raise_on_transform_error=True) return value @@ -768,7 +768,7 @@ def get_parameters( def set_parameter( - name: str, + path: str, value: str, *, # force keyword arguments type: Optional[Literal["String", "StringList", "SecureString"]] = "String", @@ -784,8 +784,8 @@ def set_parameter( Parameters ---------- - name: str - Name of the parameter + path: str + The fully qualified name includes the complete hierarchy of the parameter path and name. type: str, optional Type of the parameter. Allowed values are String, StringList, and SecureString overwrite: bool, optional @@ -801,6 +801,10 @@ def set_parameter( sdk_options: dict, optional Dictionary of options that will be passed to the Parameter Store get_parameter API call + Returns: + -------- + The version of the parameter that was set + Raises ------ SetParameterError @@ -809,16 +813,20 @@ def set_parameter( TransformParameterError When the parameter provider fails to transform a parameter value. + URLs: + ------- + https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ssm/client/put_parameter.html + Example ------- **Sets a parameter value from Systems Manager Parameter Store** - >>> from aws_lambda_powertools.utilities.parameters import set_parameter + >>> from aws_lambda_powertools.utilities import parameters >>> - >>> value = set_parameter("/my/parameter", "My parameter value", description="My parameter description") + >>> value = parameters.set_parameter(path="/my/example/parameter", value="More Powertools", description="My parameter description") >>> >>> print(value) - My parameter value + 123 """ # Only create the provider if this function is called at least once @@ -826,7 +834,7 @@ def set_parameter( DEFAULT_PROVIDERS["ssm"] = SSMProvider() return DEFAULT_PROVIDERS["ssm"].set( - name, + path, value, type=type, overwrite=overwrite, From 0abb31b03e9820fcfaaddce392f76db6ae3a9ad5 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Sat, 16 Sep 2023 14:34:04 -0400 Subject: [PATCH 17/67] fix example and return type --- aws_lambda_powertools/utilities/parameters/secrets.py | 4 ++-- aws_lambda_powertools/utilities/parameters/ssm.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index e64de578c62..5cc6f0edf35 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -299,13 +299,13 @@ def set_secret( >>> from aws_lambda_powertools.utilities import parameters >>> - >>> parameters.set_secret(name="llamas-are-awesome", secret={"password": "supers3cr3tllam@passw0rd"}) + >>> parameters.set_secret(name="llamas-are-awesome", secret="supers3cr3tllam@passw0rd") **Sets a secret and includes an idempotency_id** >>> from aws_lambda_powertools.utilities import parameters >>> - >>> parameters.set_secret("my-secret", secret='{"password": "supers3cr3tllam@passw0rd"}', idempotency_id="f658cac0-98a5-41d9-b993-8a76a7799194") + >>> parameters.set_secret(name="my-secret", secret='{"password": "supers3cr3tllam@passw0rd"}', idempotency_id="f658cac0-98a5-41d9-b993-8a76a7799194") """ # Only create the provider if this function is called at least once diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index 5b0ba076f2e..955c924469f 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -200,7 +200,7 @@ def set( kms_key_id: Optional[str] = None, transform: Optional[str] = None, **sdk_options, - ) -> str: + ) -> int: """ Retrieve a parameter value or return the cached value @@ -778,7 +778,7 @@ def set_parameter( kms_key_id: Optional[str] = None, transform: Optional[str] = None, **sdk_options, -) -> str: +) -> int: """ Retrieve a parameter value from AWS Systems Manager (SSM) Parameter Store @@ -803,7 +803,7 @@ def set_parameter( Returns: -------- - The version of the parameter that was set + The version (integer) of the parameter that was set Raises ------ @@ -823,9 +823,9 @@ def set_parameter( >>> from aws_lambda_powertools.utilities import parameters >>> - >>> value = parameters.set_parameter(path="/my/example/parameter", value="More Powertools", description="My parameter description") + >>> response = parameters.set_parameter(path="/my/example/parameter", value="More Powertools", description="My parameter description") >>> - >>> print(value) + >>> print(response) 123 """ From 299298983401f57dbb2ade3145ac5050852df4fa Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Mon, 18 Sep 2023 18:20:34 -0400 Subject: [PATCH 18/67] update --- aws_lambda_powertools/utilities/parameters/ssm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index c65d9f9ace2..4d2477c5f7e 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -193,7 +193,7 @@ def set( path: str, value: str, *, # force keyword arguments - type: Optional[str] = "String", + type: Optional[Literal["String", "StringList", "SecureString"]] = "String", overwrite: bool = False, tier: Optional[Literal["Standard", "Advanced", "Intelligent-Tiering"]] = "Standard", description: Optional[str] = None, From 887f3c470f1389da94f1812bf1fc317934c52194 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Mon, 18 Sep 2023 19:54:51 -0400 Subject: [PATCH 19/67] fix a few new warnings --- .../utilities/parameters/secrets.py | 2 +- aws_lambda_powertools/utilities/parameters/ssm.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index 5cc6f0edf35..7f90a873456 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -1,7 +1,7 @@ """ AWS Secrets Manager parameter retrieval and caching utility """ - +from __future__ import annotations import os import json diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index 4d2477c5f7e..85079ec8bd7 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -193,7 +193,7 @@ def set( path: str, value: str, *, # force keyword arguments - type: Optional[Literal["String", "StringList", "SecureString"]] = "String", + parameter_type: Optional[Literal["String", "StringList", "SecureString"]] = "String", overwrite: bool = False, tier: Optional[Literal["Standard", "Advanced", "Intelligent-Tiering"]] = "Standard", description: Optional[str] = None, @@ -208,7 +208,7 @@ def set( ---------- path: str The fully qualified name includes the complete hierarchy of the parameter path and name. - type: str, optional + parameter_type: str, optional Type of the parameter. Allowed values are String, StringList, and SecureString overwrite: bool, optional If the parameter value should be overwritten, False by default @@ -234,7 +234,7 @@ def set( sdk_options["Name"] = path sdk_options["Value"] = value - sdk_options["Type"] = type + sdk_options["Type"] = parameter_type sdk_options["Overwrite"] = overwrite sdk_options["Tier"] = tier @@ -771,7 +771,7 @@ def set_parameter( path: str, value: str, *, # force keyword arguments - type: Optional[Literal["String", "StringList", "SecureString"]] = "String", + parameter_type: Optional[Literal["String", "StringList", "SecureString"]] = "String", overwrite: bool = False, tier: Optional[Literal["Standard", "Advanced", "Intelligent-Tiering"]] = "Standard", description: Optional[str] = None, @@ -786,7 +786,7 @@ def set_parameter( ---------- path: str The fully qualified name includes the complete hierarchy of the parameter path and name. - type: str, optional + parameter_type: str, optional Type of the parameter. Allowed values are String, StringList, and SecureString overwrite: bool, optional If the parameter value should be overwritten, False by default @@ -823,7 +823,7 @@ def set_parameter( >>> from aws_lambda_powertools.utilities import parameters >>> - >>> response = parameters.set_parameter(path="/my/example/parameter", value="More Powertools", description="My parameter description") + >>> response = parameters.set_parameter(path="/my/example/parameter", value="More Powertools") >>> >>> print(response) 123 @@ -836,7 +836,7 @@ def set_parameter( return DEFAULT_PROVIDERS["ssm"].set( path, value, - type=type, + parameter_type=parameter_type, overwrite=overwrite, tier=tier, description=description, From 98c7d5f3435dd9d11c69bf356ced35ee600362cf Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Tue, 19 Sep 2023 08:49:41 -0400 Subject: [PATCH 20/67] simplying secret value --- .../utilities/parameters/secrets.py | 38 ++++++++----------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index 7f90a873456..b3c26aa72d3 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -120,10 +120,9 @@ def _get_multiple(self, path: str, **sdk_options) -> Dict[str, str]: def set_secret( self, - *, # force keyword arguments name: str, - secret: Optional[str], - secret_binary: Optional[str], + value: Union[str, bytes], + *, # force keyword arguments idempotency_id: Optional[str], version_stages: Optional[list[str]], **sdk_options @@ -135,10 +134,8 @@ def set_secret( ---------- name: str The ARN or name of the secret to add a new version to. - secret: str, optional + value: str or bytes Specifies text data that you want to encrypt and store in this new version of the secret. - secret_binary: bytes, optional - Specifies binary data that you want to encrypt and store in this new version of the secret. idempotency_id: str, optional Idempotency token to use for the request to prevent the accidental creation of duplicate versions if there are failures and retries @@ -157,18 +154,16 @@ def set_secret( https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager/client/put_secret_value.html """ - if isinstance(secret, dict): - secret = json.dumps(secret) + sdk_options["SecretId"] = name - if not isinstance(secret_binary, bytes): - secret_binary = secret_binary.encode("utf-8") + if isinstance(value, dict): + value = json.dumps(value) + + if isinstance(value, bytes): + sdk_options["SecretBinary"] = value + else: + sdk_options["SecretString"] = value - # Explicit arguments will take precedence over keyword arguments - sdk_options["SecretId"] = name - if secret: - sdk_options["SecretString"] = secret - if secret_binary: - sdk_options["SecretBinary"] = secret_binary if version_stages: sdk_options["VersionStages"] = version_stages if idempotency_id: @@ -251,10 +246,9 @@ def get_secret( def set_secret( - *, # force keyword arguments name: str, - secret: Optional[Union[str, dict]] = None, - secret_binary: Optional[bytes] = None, + value: Union[str, bytes], + *, # force keyword arguments idempotency_id: Optional[str] = None, version_stages: Optional[list[str]] = None, **sdk_options @@ -266,10 +260,8 @@ def set_secret( ---------- name: str Name of the parameter - secret: str, optional + value: str or bytes Secret value to set - secret_binary: bytes, optional - Secret binary value to set idempotency_token: str, optional Idempotency token to use for the request to prevent the accidental creation of duplicate versions if there are failures and retries @@ -313,5 +305,5 @@ def set_secret( DEFAULT_PROVIDERS["secrets"] = SecretsProvider() return DEFAULT_PROVIDERS["secrets"].set_secret( - name=name, secret=secret, secret_binary=secret_binary, idempotency_id=idempotency_id, version_stages=version_stages, **sdk_options + name=name, secret=value, idempotency_id=idempotency_id, version_stages=version_stages, **sdk_options ) From d83715471eecc806368a58b7a3d5754138d6d616 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Thu, 21 Sep 2023 07:18:36 -0400 Subject: [PATCH 21/67] small tweaks --- .../utilities/parameters/secrets.py | 5 ++--- aws_lambda_powertools/utilities/parameters/ssm.py | 14 +++++++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index b3c26aa72d3..b83c2559f27 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -170,12 +170,11 @@ def set_secret( sdk_options["ClientRequestToken"] = idempotency_id try: - value = self.client.put_secret_value(**sdk_options)["VersionId"] + value = self.client.put_secret_value(**sdk_options) + return value["VersionId"] except Exception as exc: raise SetParameterError(str(exc)) from exc - return value - def get_secret( name: str, diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index 85079ec8bd7..d14968f8e76 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -25,6 +25,9 @@ from mypy_boto3_ssm import SSMClient from mypy_boto3_ssm.type_defs import GetParametersResultTypeDef +SSM_PARAMETER_TYPES = Literal["String", "StringList", "SecureString"] +SSM_PARAMETER_TIER = Literal["Standard", "Advanced", "Intelligent-Tiering"] + class SSMProvider(BaseProvider): """ @@ -193,9 +196,9 @@ def set( path: str, value: str, *, # force keyword arguments - parameter_type: Optional[Literal["String", "StringList", "SecureString"]] = "String", + parameter_type: SSM_PARAMETER_TYPES = "String", overwrite: bool = False, - tier: Optional[Literal["Standard", "Advanced", "Intelligent-Tiering"]] = "Standard", + tier: SSM_PARAMETER_TIER = "Standard", description: Optional[str] = None, kms_key_id: Optional[str] = None, transform: Optional[str] = None, @@ -244,14 +247,15 @@ def set( sdk_options["KeyId"] = kms_key_id try: - value = self.client.put_parameter(**sdk_options)["Version"] + value = self.client.put_parameter(**sdk_options) + version = value["Version"] except Exception as exc: raise SetParameterError(str(exc)) from exc if transform: - value = transform_value(key=path, value=value, transform=transform, raise_on_transform_error=True) + version = transform_value(key=path, value=value, transform=transform, raise_on_transform_error=True) - return value + return version def _get_multiple(self, path: str, decrypt: bool = False, recursive: bool = False, **sdk_options) -> Dict[str, str]: """ From 13c9cf7eb63713b00c438ab63da7f811d326cce3 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Thu, 21 Sep 2023 07:34:25 -0400 Subject: [PATCH 22/67] cleaning up --- .../utilities/parameters/secrets.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index b83c2559f27..b7ea2399191 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -119,14 +119,14 @@ def _get_multiple(self, path: str, **sdk_options) -> Dict[str, str]: raise NotImplementedError() def set_secret( - self, - name: str, - value: Union[str, bytes], - *, # force keyword arguments - idempotency_id: Optional[str], - version_stages: Optional[list[str]], - **sdk_options - ) -> str: + self, + name: str, + value: Union[str, bytes], + *, # force keyword arguments + idempotency_id: Optional[str] = None, + version_stages: Optional[list[str]] = None, + **sdk_options + ) -> str: """ Modifies the details of a secret, including metadata and the secret value. From 5b917058d68dadddc8e1822ffaf513e4c7f7fbda Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Thu, 21 Sep 2023 07:37:30 -0400 Subject: [PATCH 23/67] missed one --- aws_lambda_powertools/utilities/parameters/ssm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index d14968f8e76..dd0fe026f01 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -775,9 +775,9 @@ def set_parameter( path: str, value: str, *, # force keyword arguments - parameter_type: Optional[Literal["String", "StringList", "SecureString"]] = "String", + parameter_type: SSM_PARAMETER_TYPES = "String", overwrite: bool = False, - tier: Optional[Literal["Standard", "Advanced", "Intelligent-Tiering"]] = "Standard", + tier: SSM_PARAMETER_TIER = "Standard", description: Optional[str] = None, kms_key_id: Optional[str] = None, transform: Optional[str] = None, From 529c0591d136bc77bc137d1b18d968dd19ab13a2 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Fri, 22 Sep 2023 07:00:10 -0400 Subject: [PATCH 24/67] cleaning up ruff --- aws_lambda_powertools/utilities/parameters/base.py | 2 +- aws_lambda_powertools/utilities/parameters/secrets.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/base.py b/aws_lambda_powertools/utilities/parameters/base.py index 9befd3ab4f4..3f3d2cbc397 100644 --- a/aws_lambda_powertools/utilities/parameters/base.py +++ b/aws_lambda_powertools/utilities/parameters/base.py @@ -29,7 +29,7 @@ from aws_lambda_powertools.shared.functions import resolve_max_age from aws_lambda_powertools.utilities.parameters.types import TransformOptions -from .exceptions import GetParameterError, SetParameterError, TransformParameterError +from .exceptions import GetParameterError, TransformParameterError if TYPE_CHECKING: from mypy_boto3_appconfigdata import AppConfigDataClient diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index b7ea2399191..f0ba4b6f8c9 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -121,7 +121,7 @@ def _get_multiple(self, path: str, **sdk_options) -> Dict[str, str]: def set_secret( self, name: str, - value: Union[str, bytes], + value: Union[str, dict, bytes], *, # force keyword arguments idempotency_id: Optional[str] = None, version_stages: Optional[list[str]] = None, @@ -296,7 +296,7 @@ def set_secret( >>> from aws_lambda_powertools.utilities import parameters >>> - >>> parameters.set_secret(name="my-secret", secret='{"password": "supers3cr3tllam@passw0rd"}', idempotency_id="f658cac0-98a5-41d9-b993-8a76a7799194") + >>> parameters.set_secret(name="my-secret", secret='{"password": "supers3cr3tl"}', idempotency_id="f658ca...99194") """ # Only create the provider if this function is called at least once From 0c03c820e5a6ea6a9d96ab159d896908b0bce65f Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Mon, 25 Sep 2023 19:17:30 -0400 Subject: [PATCH 25/67] couple of modifications --- aws_lambda_powertools/utilities/parameters/ssm.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index dd0fe026f01..a650845326d 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -191,7 +191,7 @@ def _get(self, name: str, decrypt: bool = False, **sdk_options) -> str: return self.client.get_parameter(**sdk_options)["Parameter"]["Value"] - def set( + def _set( self, path: str, value: str, @@ -201,7 +201,6 @@ def set( tier: SSM_PARAMETER_TIER = "Standard", description: Optional[str] = None, kms_key_id: Optional[str] = None, - transform: Optional[str] = None, **sdk_options, ) -> int: """ @@ -221,8 +220,6 @@ def set( The description of the parameter kms_key_id: str, optional The KMS key id to use to encrypt the parameter - transform: str, optional - Transforms the content from a JSON object ('json') or base64 binary string ('binary') sdk_options: dict, optional Dictionary of options that will be passed to the Parameter Store get_parameter API call @@ -252,9 +249,6 @@ def set( except Exception as exc: raise SetParameterError(str(exc)) from exc - if transform: - version = transform_value(key=path, value=value, transform=transform, raise_on_transform_error=True) - return version def _get_multiple(self, path: str, decrypt: bool = False, recursive: bool = False, **sdk_options) -> Dict[str, str]: @@ -837,7 +831,7 @@ def set_parameter( if "ssm" not in DEFAULT_PROVIDERS: DEFAULT_PROVIDERS["ssm"] = SSMProvider() - return DEFAULT_PROVIDERS["ssm"].set( + return DEFAULT_PROVIDERS["ssm"]._set( path, value, parameter_type=parameter_type, From 2d98d043d113a2da3d8cf6c68bf6145ca1de47ca Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Mon, 25 Sep 2023 19:31:54 -0400 Subject: [PATCH 26/67] forgot to remove this here --- aws_lambda_powertools/utilities/parameters/ssm.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index a650845326d..a90889e281e 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -839,7 +839,6 @@ def set_parameter( tier=tier, description=description, kms_key_id=kms_key_id, - transform=transform, **sdk_options ) From 7d406de278d13b42023579b681ac731b79c5ce62 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Mon, 25 Sep 2023 19:52:13 -0400 Subject: [PATCH 27/67] fix --- aws_lambda_powertools/utilities/parameters/secrets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index f0ba4b6f8c9..c6c4475bfc9 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -118,7 +118,7 @@ def _get_multiple(self, path: str, **sdk_options) -> Dict[str, str]: """ raise NotImplementedError() - def set_secret( + def _set( self, name: str, value: Union[str, dict, bytes], @@ -303,6 +303,6 @@ def set_secret( if "secrets" not in DEFAULT_PROVIDERS: DEFAULT_PROVIDERS["secrets"] = SecretsProvider() - return DEFAULT_PROVIDERS["secrets"].set_secret( - name=name, secret=value, idempotency_id=idempotency_id, version_stages=version_stages, **sdk_options + return DEFAULT_PROVIDERS["secrets"]._set( + name=name, value=value, idempotency_id=idempotency_id, version_stages=version_stages, **sdk_options ) From be6f7af160d7e71fc4476a784a799754578af6c6 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Thu, 28 Sep 2023 07:29:07 -0400 Subject: [PATCH 28/67] adding create when not existing --- aws_lambda_powertools/utilities/parameters/secrets.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index c6c4475bfc9..9d6dd2db89b 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Any, Dict, Optional, Union import boto3 +from botocore.exceptions import ClientError from botocore.config import Config if TYPE_CHECKING: @@ -172,6 +173,10 @@ def _set( try: value = self.client.put_secret_value(**sdk_options) return value["VersionId"] + except ClientError as exc: + if exc.response['Error']['Code'] == 'ResourceNotFoundException': + value = self.client.create_secret(**sdk_options) + return value["VersionId"] except Exception as exc: raise SetParameterError(str(exc)) from exc From 8fffa5855e07f6c41968742fe2591c6acb56cf03 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Thu, 28 Sep 2023 07:42:40 -0400 Subject: [PATCH 29/67] fix name --- aws_lambda_powertools/utilities/parameters/secrets.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index 9d6dd2db89b..a73eed43fdc 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -175,6 +175,8 @@ def _set( return value["VersionId"] except ClientError as exc: if exc.response['Error']['Code'] == 'ResourceNotFoundException': + sdk_options.pop("SecretId") + sdk_options["Name"] = name value = self.client.create_secret(**sdk_options) return value["VersionId"] except Exception as exc: From b576bb7fde5d963d5d49e2d8edd5cff42554dccb Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Thu, 28 Sep 2023 07:54:15 -0400 Subject: [PATCH 30/67] create flag --- aws_lambda_powertools/utilities/parameters/secrets.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index a73eed43fdc..1d4e0465868 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -126,6 +126,7 @@ def _set( *, # force keyword arguments idempotency_id: Optional[str] = None, version_stages: Optional[list[str]] = None, + create_not_exists: bool = False, **sdk_options ) -> str: """ @@ -143,6 +144,8 @@ def _set( during the Lambda rotation function processing. version_stages: list[str], optional Specifies a list of staging labels that are attached to this version of the secret. + create_not_exists: bool, optional + Create the secret if it does not exist, defaults to False sdk_options: dict, optional Dictionary of options that will be passed to the Secrets Manager update_secret API call @@ -174,7 +177,7 @@ def _set( value = self.client.put_secret_value(**sdk_options) return value["VersionId"] except ClientError as exc: - if exc.response['Error']['Code'] == 'ResourceNotFoundException': + if exc.response['Error']['Code'] == 'ResourceNotFoundException' and create_not_exists: sdk_options.pop("SecretId") sdk_options["Name"] = name value = self.client.create_secret(**sdk_options) From 4396b81157ea8753a3e53ff8aa8e1b2c16f736b0 Mon Sep 17 00:00:00 2001 From: Stephen Bawks <5246651+stephenbawks@users.noreply.github.com> Date: Thu, 28 Sep 2023 11:00:33 -0400 Subject: [PATCH 31/67] Update aws_lambda_powertools/utilities/parameters/secrets.py Co-authored-by: aradyaron <59508334+aradyaron@users.noreply.github.com> Signed-off-by: Stephen Bawks <5246651+stephenbawks@users.noreply.github.com> --- .../utilities/parameters/secrets.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index 1d4e0465868..1fb70d4758b 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -177,11 +177,20 @@ def _set( value = self.client.put_secret_value(**sdk_options) return value["VersionId"] except ClientError as exc: - if exc.response['Error']['Code'] == 'ResourceNotFoundException' and create_not_exists: - sdk_options.pop("SecretId") - sdk_options["Name"] = name - value = self.client.create_secret(**sdk_options) - return value["VersionId"] + if exc.response['Error']['Code'] != 'ResourceNotFoundException' : + raise SetParameterError('Failed to set parameter') from exc + elif not raise SetParameterError(str(exc)) from exc: + raise SetParameterError('Parameter does not exist, create before setting') from exc + + sdk_options.pop("SecretId") + sdk_options["Name"] = name + try: + value = self.client.create_secret(**sdk_options) + return value["VersionId"] + + except Exception as exc: + raise SetParameterError('Failed to create parameter') from exc + except Exception as exc: raise SetParameterError(str(exc)) from exc From 78e9f4533ec41da1beb327e3344218abc1c14f51 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Thu, 28 Sep 2023 13:46:33 -0400 Subject: [PATCH 32/67] couple refinements --- .../utilities/parameters/secrets.py | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index 1fb70d4758b..d1039323945 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -126,7 +126,7 @@ def _set( *, # force keyword arguments idempotency_id: Optional[str] = None, version_stages: Optional[list[str]] = None, - create_not_exists: bool = False, + create: bool = True, **sdk_options ) -> str: """ @@ -177,22 +177,19 @@ def _set( value = self.client.put_secret_value(**sdk_options) return value["VersionId"] except ClientError as exc: - if exc.response['Error']['Code'] != 'ResourceNotFoundException' : - raise SetParameterError('Failed to set parameter') from exc - elif not raise SetParameterError(str(exc)) from exc: - raise SetParameterError('Parameter does not exist, create before setting') from exc + if exc.response["Error"]["Code"] != "ResourceNotFoundException": + raise SetParameterError(str(exc)) from exc + elif not create: + raise SetParameterError("Parameter does not exist, create before setting or set 'create' to True.") from exc + else: + sdk_options.pop("SecretId") + sdk_options["Name"] = name + try: + value = self.client.create_secret(**sdk_options) + return value["VersionId"] + except Exception as exc: + raise SetParameterError(str(exc)) from exc - sdk_options.pop("SecretId") - sdk_options["Name"] = name - try: - value = self.client.create_secret(**sdk_options) - return value["VersionId"] - - except Exception as exc: - raise SetParameterError('Failed to create parameter') from exc - - except Exception as exc: - raise SetParameterError(str(exc)) from exc def get_secret( @@ -269,6 +266,7 @@ def set_secret( *, # force keyword arguments idempotency_id: Optional[str] = None, version_stages: Optional[list[str]] = None, + create: bool = True, **sdk_options ) -> str: """ @@ -323,5 +321,5 @@ def set_secret( DEFAULT_PROVIDERS["secrets"] = SecretsProvider() return DEFAULT_PROVIDERS["secrets"]._set( - name=name, value=value, idempotency_id=idempotency_id, version_stages=version_stages, **sdk_options + name=name, value=value, idempotency_id=idempotency_id, version_stages=version_stages, create=create, **sdk_options ) From 668ba3d3c9e6b41c3a588e541a835441cd6ae190 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Thu, 28 Sep 2023 13:53:31 -0400 Subject: [PATCH 33/67] updating docstrings --- .../utilities/parameters/secrets.py | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index d1039323945..94f52c66373 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -138,14 +138,15 @@ def _set( The ARN or name of the secret to add a new version to. value: str or bytes Specifies text data that you want to encrypt and store in this new version of the secret. - idempotency_id: str, optional - Idempotency token to use for the request to prevent the accidental - creation of duplicate versions if there are failures and retries - during the Lambda rotation function processing. + idempotency_token: str, optional + This value helps ensure idempotency. Recommended that you generate + a UUID-type value to ensure uniqueness within the specified secret. + This value becomes the VersionId of the new version. This field is + autopopulated if not provided. version_stages: list[str], optional Specifies a list of staging labels that are attached to this version of the secret. - create_not_exists: bool, optional - Create the secret if it does not exist, defaults to False + create: bool, optional + Create the secret by default unless this is set to False, defaults to True sdk_options: dict, optional Dictionary of options that will be passed to the Secrets Manager update_secret API call @@ -180,7 +181,7 @@ def _set( if exc.response["Error"]["Code"] != "ResourceNotFoundException": raise SetParameterError(str(exc)) from exc elif not create: - raise SetParameterError("Parameter does not exist, create before setting or set 'create' to True.") from exc + raise SetParameterError("Parameter does not exist, create before setting or set 'create' to True.") else: sdk_options.pop("SecretId") sdk_options["Name"] = name @@ -279,11 +280,14 @@ def set_secret( value: str or bytes Secret value to set idempotency_token: str, optional - Idempotency token to use for the request to prevent the accidental - creation of duplicate versions if there are failures and retries - during the Lambda rotation function processing. + This value helps ensure idempotency. Recommended that you generate + a UUID-type value to ensure uniqueness within the specified secret. + This value becomes the VersionId of the new version. This field is + autopopulated if not provided. version_stages: list[str], optional A list of staging labels that are attached to this version of the secret. + create: bool, optional + Create the secret by default unless this is set to False, defaults to True sdk_options: dict, optional Dictionary of options that will be passed to the get_secret_value call @@ -313,7 +317,11 @@ def set_secret( >>> from aws_lambda_powertools.utilities import parameters >>> - >>> parameters.set_secret(name="my-secret", secret='{"password": "supers3cr3tl"}', idempotency_id="f658ca...99194") + >>> parameters.set_secret( + name="my-secret", + secret='{"password": "supers3cr3tl"}', + idempotency_id="61f2af5f-5f75-44b1-a29f-0cc37af55b11" + ) """ # Only create the provider if this function is called at least once From 7413067b253acf8991bba7f9092138571b13ae57 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Thu, 28 Sep 2023 13:55:09 -0400 Subject: [PATCH 34/67] =?UTF-8?q?=F0=9F=92=84=20fix=20example=20again?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aws_lambda_powertools/utilities/parameters/secrets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index 94f52c66373..6ae17120286 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -319,7 +319,7 @@ def set_secret( >>> >>> parameters.set_secret( name="my-secret", - secret='{"password": "supers3cr3tl"}', + secret='{"password": "supers3cr3tllam@passw0rd"}', idempotency_id="61f2af5f-5f75-44b1-a29f-0cc37af55b11" ) """ From 2539382c1a4dbab8f050ed5cff0fa3e526e2d279 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Thu, 28 Sep 2023 18:44:31 -0400 Subject: [PATCH 35/67] =?UTF-8?q?=F0=9F=93=9D=20documentation=20is=20serio?= =?UTF-8?q?usly=20tough?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aws_lambda_powertools/utilities/parameters/secrets.py | 2 +- aws_lambda_powertools/utilities/parameters/ssm.py | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index 6ae17120286..acc4a268752 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -319,7 +319,7 @@ def set_secret( >>> >>> parameters.set_secret( name="my-secret", - secret='{"password": "supers3cr3tllam@passw0rd"}', + value='{"password": "supers3cr3tllam@passw0rd"}', idempotency_id="61f2af5f-5f75-44b1-a29f-0cc37af55b11" ) """ diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index a90889e281e..a324b32ae9e 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -210,6 +210,8 @@ def _set( ---------- path: str The fully qualified name includes the complete hierarchy of the parameter path and name. + value: str + The parameter value parameter_type: str, optional Type of the parameter. Allowed values are String, StringList, and SecureString overwrite: bool, optional @@ -228,8 +230,6 @@ def _set( SetParameterError When the parameter provider fails to set a parameter value for a given name. - TransformParameterError - When the parameter provider fails to transform a parameter value. """ sdk_options["Name"] = path @@ -774,7 +774,6 @@ def set_parameter( tier: SSM_PARAMETER_TIER = "Standard", description: Optional[str] = None, kms_key_id: Optional[str] = None, - transform: Optional[str] = None, **sdk_options, ) -> int: """ @@ -784,6 +783,8 @@ def set_parameter( ---------- path: str The fully qualified name includes the complete hierarchy of the parameter path and name. + value: str + The parameter value parameter_type: str, optional Type of the parameter. Allowed values are String, StringList, and SecureString overwrite: bool, optional @@ -794,8 +795,6 @@ def set_parameter( The description of the parameter kms_key_id: str, optional The KMS key id to use to encrypt the parameter - transform: str, optional - Transforms the content from a JSON object ('json') or base64 binary string ('binary') sdk_options: dict, optional Dictionary of options that will be passed to the Parameter Store get_parameter API call @@ -808,8 +807,6 @@ def set_parameter( SetParameterError When the parameter provider fails to retrieve a parameter value for a given name. - TransformParameterError - When the parameter provider fails to transform a parameter value. URLs: ------- From 682dd392c9d465e496a4ea0584c63899f08e7af6 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Thu, 28 Sep 2023 19:18:15 -0400 Subject: [PATCH 36/67] adding examples and documentation for set_param --- docs/utilities/parameters.md | 19 +++++++++++++++++++ ...etting_started_set_single_ssm_parameter.py | 12 ++++++++++++ ...ing_started_set_ssm_parameter_overwrite.py | 13 +++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 examples/parameters/src/getting_started_set_single_ssm_parameter.py create mode 100644 examples/parameters/src/getting_started_set_ssm_parameter_overwrite.py diff --git a/docs/utilities/parameters.md b/docs/utilities/parameters.md index d2d80230c77..9a7b02060ed 100644 --- a/docs/utilities/parameters.md +++ b/docs/utilities/parameters.md @@ -32,8 +32,10 @@ This utility requires additional permissions to work as expected. | SSM | **`get_parameter`**, **`SSMProvider.get`** | **`ssm:GetParameter`** | | SSM | **`get_parameters`**, **`SSMProvider.get_multiple`** | **`ssm:GetParametersByPath`** | | SSM | **`get_parameters_by_name`**, **`SSMProvider.get_parameters_by_name`** | **`ssm:GetParameter`** and **`ssm:GetParameters`** | +| SSM | **`set_parameter`** | **`ssm:PutParameter`** | | SSM | If using **`decrypt=True`** | You must add an additional permission **`kms:Decrypt`** | | Secrets | **`get_secret`**, **`SecretsProvider.get`** | **`secretsmanager:GetSecretValue`** | +| Secrets | **`set_secret`**, **`SecretsProvider.get`** | **`secretsmanager:PutSecretValue`** and or **`secretsmanager:CreateSecret`** | | DynamoDB | **`DynamoDBProvider.get`** | **`dynamodb:GetItem`** | | DynamoDB | **`DynamoDBProvider.get_multiple`** | **`dynamodb:Query`** | | AppConfig | **`get_app_config`**, **`AppConfigProvider.get_app_config`** | **`appconfig:GetLatestConfiguration`** and **`appconfig:StartConfigurationSession`** | @@ -84,6 +86,23 @@ For multiple parameters, you can use either: --8<-- "examples/parameters/src/get_parameter_by_name_error_handling.py" ``` +### Setting parameters + +You can set a parameter using the `set_parameter` high-level function. This will create a new parameter if it doesn't exist. + +=== "getting_started_set_single_ssm_parameter.py" + ```python hl_lines="8" + --8<-- "examples/parameters/src/getting_started_set_single_ssm_parameter.py" + ``` + +=== "getting_started_set_ssm_parameter_overwrite.py" + There are occasions where sometimes you are setting a parameter and then you may need to update that parameter later on. In this case, you can use the `overwrite` parameter to overwrite the parameter value if it already exists. If you do not set this parameter, then the parameter will not be overwritten and an exception will be raised. + + ```python hl_lines="8" + --8<-- "examples/parameters/src/getting_started_set_ssm_parameter_overwrite.py" + ``` + + ### Fetching secrets You can fetch secrets stored in Secrets Manager using `get_secret`. diff --git a/examples/parameters/src/getting_started_set_single_ssm_parameter.py b/examples/parameters/src/getting_started_set_single_ssm_parameter.py new file mode 100644 index 00000000000..64e86ca78a9 --- /dev/null +++ b/examples/parameters/src/getting_started_set_single_ssm_parameter.py @@ -0,0 +1,12 @@ +from aws_lambda_powertools.utilities import parameters +from aws_lambda_powertools.utilities.typing import LambdaContext + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + try: + # Set a single parameter, returns the version ID of the parameter + parameter_version = parameters.set_parameter(path="/mySuper/Parameter", value="PowerToolsIsAwesome") # type: ignore[assignment] # noqa: E501 + + return {"mySuperParameterVersion": parameter_version, "statusCode": 200} + except parameters.exceptions.SetParameterError as error: + return {"comments": None, "message": str(error), "statusCode": 400} diff --git a/examples/parameters/src/getting_started_set_ssm_parameter_overwrite.py b/examples/parameters/src/getting_started_set_ssm_parameter_overwrite.py new file mode 100644 index 00000000000..fd78050c87c --- /dev/null +++ b/examples/parameters/src/getting_started_set_ssm_parameter_overwrite.py @@ -0,0 +1,13 @@ +from aws_lambda_powertools.utilities import parameters +from aws_lambda_powertools.utilities.typing import LambdaContext + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + try: + # Set a single parameter, but overwrite if it already exists + # by default, overwrite is False, explicitly set it to True + updating_parameter = parameters.set_parameter(path="/mySuper/Parameter", value="PowerToolsIsAwesome", overwrite=True) # type: ignore[assignment] # noqa: E501 + + return {"mySuperParameterVersion": updating_parameter, "statusCode": 200} + except parameters.exceptions.SetParameterError as error: + return {"comments": None, "message": str(error), "statusCode": 400} From d264b653720e5f4d7f191cf5a17d51944d90e1d5 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Fri, 29 Sep 2023 14:14:57 -0400 Subject: [PATCH 37/67] trying to add some docs --- .../utilities/parameters/secrets.py | 2 +- docs/utilities/parameters.md | 10 ++++++++ .../src/getting_started_setting_secret.py | 25 +++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 examples/parameters/src/getting_started_setting_secret.py diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index acc4a268752..148cb4a5255 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -311,7 +311,7 @@ def set_secret( >>> from aws_lambda_powertools.utilities import parameters >>> - >>> parameters.set_secret(name="llamas-are-awesome", secret="supers3cr3tllam@passw0rd") + >>> parameters.set_secret(name="llamas-are-awesome", value="supers3cr3tllam@passw0rd") **Sets a secret and includes an idempotency_id** diff --git a/docs/utilities/parameters.md b/docs/utilities/parameters.md index 9a7b02060ed..280641e8a93 100644 --- a/docs/utilities/parameters.md +++ b/docs/utilities/parameters.md @@ -112,6 +112,16 @@ You can fetch secrets stored in Secrets Manager using `get_secret`. --8<-- "examples/parameters/src/getting_started_secret.py" ``` +### Setting secrets + +You can set secrets stored in Secrets Manager using `set_secret`. + +=== "getting_started_secret.py" + ```python hl_lines="5 15" + --8<-- "examples/parameters/src/getting_started_setting_secret.py" + ``` + + ### Fetching app configurations You can fetch application configurations in AWS AppConfig using `get_app_config`. diff --git a/examples/parameters/src/getting_started_setting_secret.py b/examples/parameters/src/getting_started_setting_secret.py new file mode 100644 index 00000000000..8643d4cab6f --- /dev/null +++ b/examples/parameters/src/getting_started_setting_secret.py @@ -0,0 +1,25 @@ +from typing import Any + +import requests + +from aws_lambda_powertools.utilities import parameters +from aws_lambda_powertools.utilities.typing import LambdaContext + + +def lambda_handler(event: dict, context: LambdaContext): + + try: + # Usually an endpoint is not sensitive data, so we store it in SSM Parameters + endpoint_comments: Any = parameters.get_parameter("/lambda-powertools/endpoint_comments") + + # An API-KEY is a sensitive data and should be stored in SecretsManager + # set-secret will create a new secret if it doesn't exist and return the version id + update_secret = parameters.set_secret(name="/lambda-powertools/api-key", value="3884c335-25b0-4267-8531-561777eb2078") + + headers: dict = {"X-API-Key": api_key} + + comments: requests.Response = requests.get(endpoint_comments, headers=headers) + + return {"comments": comments.json()[:10], "statusCode": 200} + except parameters.exceptions.GetParameterError as error: + return {"comments": None, "message": str(error), "statusCode": 400} From 7a488bc432e8f64cfe6e2ecf41c0fc09aeb0e946 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Sat, 30 Sep 2023 10:56:03 -0400 Subject: [PATCH 38/67] creating "realistic" example --- .../src/getting_started_setting_secret.py | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/examples/parameters/src/getting_started_setting_secret.py b/examples/parameters/src/getting_started_setting_secret.py index 8643d4cab6f..f16472fcdc4 100644 --- a/examples/parameters/src/getting_started_setting_secret.py +++ b/examples/parameters/src/getting_started_setting_secret.py @@ -6,20 +6,25 @@ from aws_lambda_powertools.utilities.typing import LambdaContext +def access_token(client_id: str, client_secret: str, audience: str) -> str: + # example function that returns a JWT access token + ... + + return access_token + def lambda_handler(event: dict, context: LambdaContext): try: # Usually an endpoint is not sensitive data, so we store it in SSM Parameters - endpoint_comments: Any = parameters.get_parameter("/lambda-powertools/endpoint_comments") + client_id: Any = parameters.get_parameter("/lambda-powertools/client_id") + client_secret: Any = parameters.get_parameter("/lambda-powertools/client_secret") + audience: Any = parameters.get_parameter("/lambda-powertools/audience") - # An API-KEY is a sensitive data and should be stored in SecretsManager - # set-secret will create a new secret if it doesn't exist and return the version id - update_secret = parameters.set_secret(name="/lambda-powertools/api-key", value="3884c335-25b0-4267-8531-561777eb2078") + jwt_token = access_token(client_id=client_id, client_secret=client_secret, audience=audience) - headers: dict = {"X-API-Key": api_key} - - comments: requests.Response = requests.get(endpoint_comments, headers=headers) + # set-secret will create a new secret if it doesn't exist and return the version id + update_secret = parameters.set_secret(name="/lambda-powertools/api-key", value=jwt_token) - return {"comments": comments.json()[:10], "statusCode": 200} + return {"access_token": "updated", "statusCode": 200} except parameters.exceptions.GetParameterError as error: - return {"comments": None, "message": str(error), "statusCode": 400} + return {"access_token": "updated", "statusCode": 400} From 6915419ca30fbd5a79a5d2f08316bcf88620dba8 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Sat, 14 Oct 2023 15:30:59 -0400 Subject: [PATCH 39/67] updating example --- .../src/getting_started_setting_secret.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/examples/parameters/src/getting_started_setting_secret.py b/examples/parameters/src/getting_started_setting_secret.py index f16472fcdc4..660a82e7c66 100644 --- a/examples/parameters/src/getting_started_setting_secret.py +++ b/examples/parameters/src/getting_started_setting_secret.py @@ -1,21 +1,19 @@ from typing import Any -import requests - +from aws_lambda_powertools import Logger from aws_lambda_powertools.utilities import parameters from aws_lambda_powertools.utilities.typing import LambdaContext +logger = Logger(serialize_stacktrace=True) + def access_token(client_id: str, client_secret: str, audience: str) -> str: # example function that returns a JWT access token ... - return access_token def lambda_handler(event: dict, context: LambdaContext): - try: - # Usually an endpoint is not sensitive data, so we store it in SSM Parameters client_id: Any = parameters.get_parameter("/lambda-powertools/client_id") client_secret: Any = parameters.get_parameter("/lambda-powertools/client_secret") audience: Any = parameters.get_parameter("/lambda-powertools/audience") @@ -23,8 +21,9 @@ def lambda_handler(event: dict, context: LambdaContext): jwt_token = access_token(client_id=client_id, client_secret=client_secret, audience=audience) # set-secret will create a new secret if it doesn't exist and return the version id - update_secret = parameters.set_secret(name="/lambda-powertools/api-key", value=jwt_token) + update_secret_version_id = parameters.set_secret(name="/lambda-powertools/api-key", value=jwt_token) - return {"access_token": "updated", "statusCode": 200} + return {"access_token": "updated", "statusCode": 200, "update_secret_version_id": update_secret_version_id} except parameters.exceptions.GetParameterError as error: + logger.exception(error) return {"access_token": "updated", "statusCode": 400} From 492027451545043e8a6d950d1b7bcc978f461907 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Sat, 14 Oct 2023 15:34:47 -0400 Subject: [PATCH 40/67] making example more appropiate --- .../src/getting_started_setting_secret.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/examples/parameters/src/getting_started_setting_secret.py b/examples/parameters/src/getting_started_setting_secret.py index 660a82e7c66..0f4fe69bda4 100644 --- a/examples/parameters/src/getting_started_setting_secret.py +++ b/examples/parameters/src/getting_started_setting_secret.py @@ -4,26 +4,25 @@ from aws_lambda_powertools.utilities import parameters from aws_lambda_powertools.utilities.typing import LambdaContext - logger = Logger(serialize_stacktrace=True) def access_token(client_id: str, client_secret: str, audience: str) -> str: - # example function that returns a JWT access token + # example function that returns a JWT Access Token ... return access_token def lambda_handler(event: dict, context: LambdaContext): try: - client_id: Any = parameters.get_parameter("/lambda-powertools/client_id") - client_secret: Any = parameters.get_parameter("/lambda-powertools/client_secret") - audience: Any = parameters.get_parameter("/lambda-powertools/audience") + client_id: Any = parameters.get_parameter("/aws-powertools/client_id") + client_secret: Any = parameters.get_parameter("/aws-powertools/client_secret") + audience: Any = parameters.get_parameter("/aws-powertools/audience") jwt_token = access_token(client_id=client_id, client_secret=client_secret, audience=audience) # set-secret will create a new secret if it doesn't exist and return the version id - update_secret_version_id = parameters.set_secret(name="/lambda-powertools/api-key", value=jwt_token) + update_secret_version_id = parameters.set_secret(name="/lambda-powertools/jwt_token", value=jwt_token) return {"access_token": "updated", "statusCode": 200, "update_secret_version_id": update_secret_version_id} - except parameters.exceptions.GetParameterError as error: + except parameters.exceptions.SetParameterError as error: logger.exception(error) return {"access_token": "updated", "statusCode": 400} From abce28e80563073dc195dec4f28ee87897785a37 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Sat, 14 Oct 2023 15:35:16 -0400 Subject: [PATCH 41/67] missed one --- examples/parameters/src/getting_started_setting_secret.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/parameters/src/getting_started_setting_secret.py b/examples/parameters/src/getting_started_setting_secret.py index 0f4fe69bda4..b1f6c9d23de 100644 --- a/examples/parameters/src/getting_started_setting_secret.py +++ b/examples/parameters/src/getting_started_setting_secret.py @@ -20,7 +20,7 @@ def lambda_handler(event: dict, context: LambdaContext): jwt_token = access_token(client_id=client_id, client_secret=client_secret, audience=audience) # set-secret will create a new secret if it doesn't exist and return the version id - update_secret_version_id = parameters.set_secret(name="/lambda-powertools/jwt_token", value=jwt_token) + update_secret_version_id = parameters.set_secret(name="/aws-powertools/jwt_token", value=jwt_token) return {"access_token": "updated", "statusCode": 200, "update_secret_version_id": update_secret_version_id} except parameters.exceptions.SetParameterError as error: From d49f89ff96d60fba45199a816a8d12bfa9fef5e8 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Thu, 19 Oct 2023 08:28:13 -0400 Subject: [PATCH 42/67] couple tests so far --- tests/functional/test_utilities_parameters.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/tests/functional/test_utilities_parameters.py b/tests/functional/test_utilities_parameters.py index 7822ff80949..da72e0b0a7a 100644 --- a/tests/functional/test_utilities_parameters.py +++ b/tests/functional/test_utilities_parameters.py @@ -510,6 +510,72 @@ def test_ssm_provider_get(mock_name, mock_value, mock_version, config): finally: stubber.deactivate() +def test_ssm_provider_set(mock_name, mock_value, mock_version, config): + """ + Test SSMProvider.set_parameter() with a non-cached value + """ + # Create a new provider + provider = parameters.SSMProvider(config=config) + + # Stub the boto3 client + stubber = stub.Stubber(provider.client) + response = { + "Version": mock_version, + "Tier": "Standard" + } + expected_params = { + "Name": mock_name, + "Value": mock_value, + "Type": "String", + "Overwrite": False, + "Tier": "Standard", + } + stubber.add_response("put_parameter", response, expected_params) + stubber.activate() + + try: + version = provider._set(mock_name, mock_value) + + assert version == mock_version + stubber.assert_no_pending_responses() + finally: + stubber.deactivate() + + +def test_ssm_provider_set_default_config(monkeypatch, mock_name, mock_value, mock_version): + """ + Test SSMProvider._set() without specifying the config + """ + + monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-2") + + # Create a new provider + provider = parameters.SSMProvider() + + # Stub the boto3 client + stubber = stub.Stubber(provider.client) + response = { + "Version": mock_version, + "Tier": "Advanced" + } + expected_params = { + "Name": mock_name, + "Value": mock_value, + "Type": "SecureString", + "Overwrite": False, + "Tier": "Advanced", + } + stubber.add_response("put_parameter", response, expected_params) + stubber.activate() + + try: + version = provider._set(mock_name, mock_value) + + assert version == mock_version + stubber.assert_no_pending_responses() + finally: + stubber.deactivate() + def test_ssm_provider_get_with_custom_client(mock_name, mock_value, mock_version, config): """ From f9a29444498d86d1c2c927cd282ef3c5503f6f71 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Sat, 17 Feb 2024 09:15:13 -0500 Subject: [PATCH 43/67] changing variable name --- .../utilities/parameters/secrets.py | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index 148cb4a5255..81972d9906f 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -3,13 +3,13 @@ """ from __future__ import annotations -import os import json +import os from typing import TYPE_CHECKING, Any, Dict, Optional, Union import boto3 -from botocore.exceptions import ClientError from botocore.config import Config +from botocore.exceptions import ClientError if TYPE_CHECKING: from mypy_boto3_secretsmanager import SecretsManagerClient @@ -123,11 +123,11 @@ def _set( self, name: str, value: Union[str, dict, bytes], - *, # force keyword arguments - idempotency_id: Optional[str] = None, + *, # force keyword arguments + client_request_token: Optional[str] = None, version_stages: Optional[list[str]] = None, create: bool = True, - **sdk_options + **sdk_options, ) -> str: """ Modifies the details of a secret, including metadata and the secret value. @@ -138,7 +138,7 @@ def _set( The ARN or name of the secret to add a new version to. value: str or bytes Specifies text data that you want to encrypt and store in this new version of the secret. - idempotency_token: str, optional + client_request_token: str, optional This value helps ensure idempotency. Recommended that you generate a UUID-type value to ensure uniqueness within the specified secret. This value becomes the VersionId of the new version. This field is @@ -171,8 +171,8 @@ def _set( if version_stages: sdk_options["VersionStages"] = version_stages - if idempotency_id: - sdk_options["ClientRequestToken"] = idempotency_id + if client_request_token: + sdk_options["ClientRequestToken"] = client_request_token try: value = self.client.put_secret_value(**sdk_options) @@ -192,7 +192,6 @@ def _set( raise SetParameterError(str(exc)) from exc - def get_secret( name: str, transform: Optional[str] = None, @@ -264,11 +263,11 @@ def get_secret( def set_secret( name: str, value: Union[str, bytes], - *, # force keyword arguments - idempotency_id: Optional[str] = None, + *, # force keyword arguments + client_request_token: Optional[str] = None, version_stages: Optional[list[str]] = None, create: bool = True, - **sdk_options + **sdk_options, ) -> str: """ Retrieve a parameter value from AWS Secrets Manager @@ -279,7 +278,7 @@ def set_secret( Name of the parameter value: str or bytes Secret value to set - idempotency_token: str, optional + client_request_token: str, optional This value helps ensure idempotency. Recommended that you generate a UUID-type value to ensure uniqueness within the specified secret. This value becomes the VersionId of the new version. This field is @@ -313,14 +312,14 @@ def set_secret( >>> >>> parameters.set_secret(name="llamas-are-awesome", value="supers3cr3tllam@passw0rd") - **Sets a secret and includes an idempotency_id** + **Sets a secret and includes an client_request_token** >>> from aws_lambda_powertools.utilities import parameters >>> >>> parameters.set_secret( name="my-secret", value='{"password": "supers3cr3tllam@passw0rd"}', - idempotency_id="61f2af5f-5f75-44b1-a29f-0cc37af55b11" + client_request_token="61f2af5f-5f75-44b1-a29f-0cc37af55b11" ) """ @@ -329,5 +328,10 @@ def set_secret( DEFAULT_PROVIDERS["secrets"] = SecretsProvider() return DEFAULT_PROVIDERS["secrets"]._set( - name=name, value=value, idempotency_id=idempotency_id, version_stages=version_stages, create=create, **sdk_options + name=name, + value=value, + client_request_token=client_request_token, + version_stages=version_stages, + create=create, + **sdk_options, ) From d55a9b431f3106a7db15535df8e5c391ee84c3d5 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Sat, 17 Feb 2024 09:26:36 -0500 Subject: [PATCH 44/67] removing the create logic for the time being --- aws_lambda_powertools/utilities/parameters/secrets.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index 81972d9906f..c846db907eb 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -180,16 +180,6 @@ def _set( except ClientError as exc: if exc.response["Error"]["Code"] != "ResourceNotFoundException": raise SetParameterError(str(exc)) from exc - elif not create: - raise SetParameterError("Parameter does not exist, create before setting or set 'create' to True.") - else: - sdk_options.pop("SecretId") - sdk_options["Name"] = name - try: - value = self.client.create_secret(**sdk_options) - return value["VersionId"] - except Exception as exc: - raise SetParameterError(str(exc)) from exc def get_secret( From 9ead47a93c3dbbcfd40c38d906f104505058c218 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Mon, 19 Feb 2024 10:06:04 -0500 Subject: [PATCH 45/67] remove create option --- aws_lambda_powertools/utilities/parameters/secrets.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index c846db907eb..61e06450bff 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -126,7 +126,6 @@ def _set( *, # force keyword arguments client_request_token: Optional[str] = None, version_stages: Optional[list[str]] = None, - create: bool = True, **sdk_options, ) -> str: """ @@ -145,8 +144,6 @@ def _set( autopopulated if not provided. version_stages: list[str], optional Specifies a list of staging labels that are attached to this version of the secret. - create: bool, optional - Create the secret by default unless this is set to False, defaults to True sdk_options: dict, optional Dictionary of options that will be passed to the Secrets Manager update_secret API call @@ -256,7 +253,6 @@ def set_secret( *, # force keyword arguments client_request_token: Optional[str] = None, version_stages: Optional[list[str]] = None, - create: bool = True, **sdk_options, ) -> str: """ @@ -275,8 +271,6 @@ def set_secret( autopopulated if not provided. version_stages: list[str], optional A list of staging labels that are attached to this version of the secret. - create: bool, optional - Create the secret by default unless this is set to False, defaults to True sdk_options: dict, optional Dictionary of options that will be passed to the get_secret_value call @@ -322,6 +316,5 @@ def set_secret( value=value, client_request_token=client_request_token, version_stages=version_stages, - create=create, **sdk_options, ) From 7783aad90a1e847bc904d113009c59a26a7a8ecb Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Mon, 26 Feb 2024 17:18:05 +0100 Subject: [PATCH 46/67] chore: remove set as mandatory method (temporarily) Signed-off-by: heitorlessa --- aws_lambda_powertools/utilities/parameters/base.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/base.py b/aws_lambda_powertools/utilities/parameters/base.py index 3f3d2cbc397..cb40f8ded39 100644 --- a/aws_lambda_powertools/utilities/parameters/base.py +++ b/aws_lambda_powertools/utilities/parameters/base.py @@ -1,6 +1,7 @@ """ Base for Parameter providers """ + from __future__ import annotations import base64 @@ -153,7 +154,6 @@ def _get(self, name: str, **sdk_options) -> Union[str, bytes, Dict[str, Any]]: """ raise NotImplementedError() - @abstractmethod def _set(self, name: str, **sdk_options) -> Union[str, bytes]: """ Sets a parameter value from the underlying parameter store @@ -379,8 +379,7 @@ def transform_value( transform: TransformOptions, raise_on_transform_error: bool = False, key: str = "", -) -> Dict[str, Any]: - ... +) -> Dict[str, Any]: ... @overload @@ -389,8 +388,7 @@ def transform_value( transform: TransformOptions, raise_on_transform_error: bool = False, key: str = "", -) -> Optional[Union[str, bytes, Dict[str, Any]]]: - ... +) -> Optional[Union[str, bytes, Dict[str, Any]]]: ... def transform_value( From 7325bc78fd3ceac2c8d4db7651e75d9aae09d7cf Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Tue, 25 Jul 2023 14:39:50 +0200 Subject: [PATCH 47/67] fix(parameters): make cache aware of single vs multiple calls Signed-off-by: heitorlessa --- aws_lambda_powertools/utilities/parameters/base.py | 2 +- aws_lambda_powertools/utilities/parameters/types.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/aws_lambda_powertools/utilities/parameters/base.py b/aws_lambda_powertools/utilities/parameters/base.py index 5ce06589613..334f1a9dbd6 100644 --- a/aws_lambda_powertools/utilities/parameters/base.py +++ b/aws_lambda_powertools/utilities/parameters/base.py @@ -28,7 +28,7 @@ from aws_lambda_powertools.shared import constants, user_agent from aws_lambda_powertools.shared.functions import resolve_max_age -from aws_lambda_powertools.utilities.parameters.types import TransformOptions +from aws_lambda_powertools.utilities.parameters.types import RecursiveOptions, TransformOptions from .exceptions import GetParameterError, TransformParameterError diff --git a/aws_lambda_powertools/utilities/parameters/types.py b/aws_lambda_powertools/utilities/parameters/types.py index faa06cee89e..a916f1a344d 100644 --- a/aws_lambda_powertools/utilities/parameters/types.py +++ b/aws_lambda_powertools/utilities/parameters/types.py @@ -1,3 +1,4 @@ from aws_lambda_powertools.shared.types import Literal TransformOptions = Literal["json", "binary", "auto", None] +RecursiveOptions = Literal[True, False] From 8704337b73fa5c5b0b1ef5434c0ee4b2d6094984 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Tue, 25 Jul 2023 15:16:51 +0200 Subject: [PATCH 48/67] chore: cleanup, add test for single and nested Signed-off-by: heitorlessa --- aws_lambda_powertools/utilities/parameters/base.py | 2 +- aws_lambda_powertools/utilities/parameters/types.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/base.py b/aws_lambda_powertools/utilities/parameters/base.py index 334f1a9dbd6..5ce06589613 100644 --- a/aws_lambda_powertools/utilities/parameters/base.py +++ b/aws_lambda_powertools/utilities/parameters/base.py @@ -28,7 +28,7 @@ from aws_lambda_powertools.shared import constants, user_agent from aws_lambda_powertools.shared.functions import resolve_max_age -from aws_lambda_powertools.utilities.parameters.types import RecursiveOptions, TransformOptions +from aws_lambda_powertools.utilities.parameters.types import TransformOptions from .exceptions import GetParameterError, TransformParameterError diff --git a/aws_lambda_powertools/utilities/parameters/types.py b/aws_lambda_powertools/utilities/parameters/types.py index a916f1a344d..faa06cee89e 100644 --- a/aws_lambda_powertools/utilities/parameters/types.py +++ b/aws_lambda_powertools/utilities/parameters/types.py @@ -1,4 +1,3 @@ from aws_lambda_powertools.shared.types import Literal TransformOptions = Literal["json", "binary", "auto", None] -RecursiveOptions = Literal[True, False] From cd116bbe2963202d9dac0f044b159a5f64cd8a6a Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Tue, 27 Feb 2024 17:29:30 +0100 Subject: [PATCH 49/67] refactor: implement set() minimum contract, and set() in ssm Signed-off-by: heitorlessa --- .../utilities/parameters/base.py | 5 +- .../utilities/parameters/ssm.py | 71 ++++++++++++++++++- 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/base.py b/aws_lambda_powertools/utilities/parameters/base.py index cb40f8ded39..1446646c55e 100644 --- a/aws_lambda_powertools/utilities/parameters/base.py +++ b/aws_lambda_powertools/utilities/parameters/base.py @@ -154,10 +154,7 @@ def _get(self, name: str, **sdk_options) -> Union[str, bytes, Dict[str, Any]]: """ raise NotImplementedError() - def _set(self, name: str, **sdk_options) -> Union[str, bytes]: - """ - Sets a parameter value from the underlying parameter store - """ + def set(self, name: str, value: Any, *, overwrite: bool = False, **kwargs): raise NotImplementedError() def get_multiple( diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index 9970b1f51cd..1f3bbc894bd 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -172,6 +172,75 @@ def get( # type: ignore[override] return super().get(name, max_age, transform, force_fetch, **sdk_options) + @overload + def set( + self, + name: str, + value: list[str], + *, + overwrite: bool = False, + description: str = "", + parameter_type: Literal["StringList"] = "StringList", + tier: Literal["Standard", "Advanced", "Intelligent-Tiering"] = "Standard", + kms_key_id: str | None = "None", + **sdk_options, + ): ... + + @overload + def set( + self, + name: str, + value: str, + *, + overwrite: bool = False, + description: str = "", + parameter_type: Literal["SecureString"] = "SecureString", + tier: Literal["Standard", "Advanced", "Intelligent-Tiering"] = "Standard", + kms_key_id: str, + **sdk_options, + ): ... + + @overload + def set( + self, + name: str, + value: str, + *, + overwrite: bool = False, + description: str = "", + parameter_type: Literal["String"] = "String", + tier: Literal["Standard", "Advanced", "Intelligent-Tiering"] = "Standard", + kms_key_id: str | None = None, + **sdk_options, + ): ... + + def set( + self, + name: str, + value: str | list[str], + *, + overwrite: bool = False, + description: str = "", + parameter_type: SSM_PARAMETER_TYPES = "String", + tier: SSM_PARAMETER_TIER = "Standard", + kms_key_id: str | None = None, + **sdk_options, + ): + opts = { + "Name": name, + "Value": value, + "Overwrite": overwrite, + "Type": parameter_type, + "Tier": tier, + "Description": description, + **sdk_options, + } + + if kms_key_id: + opts["KeyId"] = kms_key_id + + return self.client.put_parameter(**opts) + def _get(self, name: str, decrypt: bool = False, **sdk_options) -> str: """ Retrieve a parameter value from AWS Systems Manager Parameter Store @@ -877,7 +946,7 @@ def set_parameter( if "ssm" not in DEFAULT_PROVIDERS: DEFAULT_PROVIDERS["ssm"] = SSMProvider() - return DEFAULT_PROVIDERS["ssm"]._set( + return DEFAULT_PROVIDERS["ssm"].set( path, value, parameter_type=parameter_type, From 5ccb953a2a3be60c73d79e5e2387b675a53e46b4 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Tue, 27 Feb 2024 17:35:38 +0100 Subject: [PATCH 50/67] refactor: use name over path in set_parameter Signed-off-by: heitorlessa --- aws_lambda_powertools/utilities/parameters/ssm.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index 1f3bbc894bd..f6028efcf40 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -884,7 +884,7 @@ def get_parameters( def set_parameter( - path: str, + name: str, value: str, *, # force keyword arguments parameter_type: SSM_PARAMETER_TYPES = "String", @@ -899,8 +899,8 @@ def set_parameter( Parameters ---------- - path: str - The fully qualified name includes the complete hierarchy of the parameter path and name. + name: str + The fully qualified name includes the complete hierarchy of the parameter name and name. value: str The parameter value parameter_type: str, optional @@ -936,7 +936,7 @@ def set_parameter( >>> from aws_lambda_powertools.utilities import parameters >>> - >>> response = parameters.set_parameter(path="/my/example/parameter", value="More Powertools") + >>> response = parameters.set_parameter(name="/my/example/parameter", value="More Powertools") >>> >>> print(response) 123 @@ -947,7 +947,7 @@ def set_parameter( DEFAULT_PROVIDERS["ssm"] = SSMProvider() return DEFAULT_PROVIDERS["ssm"].set( - path, + name, value, parameter_type=parameter_type, overwrite=overwrite, From d9cf5fd8cc26cfa4432d8ed9a46deb5fd8ce4282 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Wed, 20 Mar 2024 16:42:03 +0000 Subject: [PATCH 51/67] Adding docstring --- aws_lambda_powertools/utilities/parameters/ssm.py | 14 +++++++------- tests/functional/test_utilities_parameters.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index f6028efcf40..a24656b5b94 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: from mypy_boto3_ssm import SSMClient - from mypy_boto3_ssm.type_defs import GetParametersResultTypeDef + from mypy_boto3_ssm.type_defs import GetParametersResultTypeDef, PutParameterResultTypeDef SSM_PARAMETER_TYPES = Literal["String", "StringList", "SecureString"] SSM_PARAMETER_TIER = Literal["Standard", "Advanced", "Intelligent-Tiering"] @@ -225,7 +225,7 @@ def set( tier: SSM_PARAMETER_TIER = "Standard", kms_key_id: str | None = None, **sdk_options, - ): + ) -> PutParameterResultTypeDef: opts = { "Name": name, "Value": value, @@ -895,7 +895,7 @@ def set_parameter( **sdk_options, ) -> int: """ - Retrieve a parameter value from AWS Systems Manager (SSM) Parameter Store + Sets a parameter in AWS Systems Manager Parameter Store. Parameters ---------- @@ -916,10 +916,6 @@ def set_parameter( sdk_options: dict, optional Dictionary of options that will be passed to the Parameter Store get_parameter API call - Returns: - -------- - The version (integer) of the parameter that was set - Raises ------ SetParameterError @@ -940,6 +936,10 @@ def set_parameter( >>> >>> print(response) 123 + + Returns + ------- + int: The status code indicating the success or failure of the operation. """ # Only create the provider if this function is called at least once diff --git a/tests/functional/test_utilities_parameters.py b/tests/functional/test_utilities_parameters.py index 9aefe87d85e..44ddc33e2c1 100644 --- a/tests/functional/test_utilities_parameters.py +++ b/tests/functional/test_utilities_parameters.py @@ -532,7 +532,7 @@ def test_ssm_provider_set(mock_name, mock_value, mock_version, config): stubber.activate() try: - version = provider._set(mock_name, mock_value) + version = provider.set(mock_name, mock_value) assert version == mock_version stubber.assert_no_pending_responses() From ae8975f28d1118df70597b82505eeff3cffea4af Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Wed, 20 Mar 2024 17:16:26 +0000 Subject: [PATCH 52/67] Fixing description type + adding TypeDict for set_parameter --- .../utilities/parameters/ssm.py | 24 +++++++++++-------- .../utilities/parameters/types.py | 8 ++++++- ...ing_started_set_ssm_parameter_overwrite.py | 2 +- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index a24656b5b94..04ea8bac5f3 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -17,14 +17,18 @@ slice_dictionary, ) from aws_lambda_powertools.shared.types import Literal - -from .base import DEFAULT_MAX_AGE_SECS, DEFAULT_PROVIDERS, BaseProvider, transform_value -from .exceptions import GetParameterError -from .types import TransformOptions +from aws_lambda_powertools.utilities.parameters.base import ( + DEFAULT_MAX_AGE_SECS, + DEFAULT_PROVIDERS, + BaseProvider, + transform_value, +) +from aws_lambda_powertools.utilities.parameters.exceptions import GetParameterError +from aws_lambda_powertools.utilities.parameters.types import PutParameterResponse, TransformOptions if TYPE_CHECKING: from mypy_boto3_ssm import SSMClient - from mypy_boto3_ssm.type_defs import GetParametersResultTypeDef, PutParameterResultTypeDef + from mypy_boto3_ssm.type_defs import GetParametersResultTypeDef SSM_PARAMETER_TYPES = Literal["String", "StringList", "SecureString"] SSM_PARAMETER_TIER = Literal["Standard", "Advanced", "Intelligent-Tiering"] @@ -225,7 +229,7 @@ def set( tier: SSM_PARAMETER_TIER = "Standard", kms_key_id: str | None = None, **sdk_options, - ) -> PutParameterResultTypeDef: + ) -> PutParameterResponse: opts = { "Name": name, "Value": value, @@ -887,13 +891,13 @@ def set_parameter( name: str, value: str, *, # force keyword arguments - parameter_type: SSM_PARAMETER_TYPES = "String", overwrite: bool = False, + description: str = "", + parameter_type: SSM_PARAMETER_TYPES = "String", tier: SSM_PARAMETER_TIER = "Standard", - description: Optional[str] = None, - kms_key_id: Optional[str] = None, + kms_key_id: str | None = None, **sdk_options, -) -> int: +) -> PutParameterResponse: """ Sets a parameter in AWS Systems Manager Parameter Store. diff --git a/aws_lambda_powertools/utilities/parameters/types.py b/aws_lambda_powertools/utilities/parameters/types.py index faa06cee89e..a9b0e702aff 100644 --- a/aws_lambda_powertools/utilities/parameters/types.py +++ b/aws_lambda_powertools/utilities/parameters/types.py @@ -1,3 +1,9 @@ -from aws_lambda_powertools.shared.types import Literal +from aws_lambda_powertools.shared.types import Literal, TypedDict TransformOptions = Literal["json", "binary", "auto", None] + + +class PutParameterResponse(TypedDict): + Version: int + Tier: str + ResponseMetadata: dict diff --git a/examples/parameters/src/getting_started_set_ssm_parameter_overwrite.py b/examples/parameters/src/getting_started_set_ssm_parameter_overwrite.py index fd78050c87c..53ef035c3b4 100644 --- a/examples/parameters/src/getting_started_set_ssm_parameter_overwrite.py +++ b/examples/parameters/src/getting_started_set_ssm_parameter_overwrite.py @@ -6,7 +6,7 @@ def lambda_handler(event: dict, context: LambdaContext) -> dict: try: # Set a single parameter, but overwrite if it already exists # by default, overwrite is False, explicitly set it to True - updating_parameter = parameters.set_parameter(path="/mySuper/Parameter", value="PowerToolsIsAwesome", overwrite=True) # type: ignore[assignment] # noqa: E501 + updating_parameter = parameters.set_parameter(name="/mySuper/Parameter", value="PowerToolsIsAwesome", overwrite=True) # type: ignore[assignment] # noqa: E501 return {"mySuperParameterVersion": updating_parameter, "statusCode": 200} except parameters.exceptions.SetParameterError as error: From 6502229ed10d9988cf09ab115a9cc28c0d3e1667 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Wed, 20 Mar 2024 17:53:56 +0000 Subject: [PATCH 53/67] Refactoring tests --- .../utilities/parameters/__init__.py | 2 +- .../utilities/parameters/ssm.py | 3 + docs/utilities/parameters.md | 2 - tests/functional/test_utilities_parameters.py | 56 +++++++++++++++++-- 4 files changed, 55 insertions(+), 8 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/__init__.py b/aws_lambda_powertools/utilities/parameters/__init__.py index f07787769e7..9f8827ed9b6 100644 --- a/aws_lambda_powertools/utilities/parameters/__init__.py +++ b/aws_lambda_powertools/utilities/parameters/__init__.py @@ -9,7 +9,7 @@ from .dynamodb import DynamoDBProvider from .exceptions import GetParameterError, TransformParameterError from .secrets import SecretsProvider, get_secret, set_secret -from .ssm import SSMProvider, get_parameter, set_parameter, get_parameters, get_parameters_by_name +from .ssm import SSMProvider, get_parameter, get_parameters, get_parameters_by_name, set_parameter __all__ = [ "AppConfigProvider", diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index 04ea8bac5f3..2f21b1e1756 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -4,6 +4,7 @@ from __future__ import annotations +import logging import os from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, overload @@ -33,6 +34,8 @@ SSM_PARAMETER_TYPES = Literal["String", "StringList", "SecureString"] SSM_PARAMETER_TIER = Literal["Standard", "Advanced", "Intelligent-Tiering"] +logger = logging.getLogger(__name__) + class SSMProvider(BaseProvider): """ diff --git a/docs/utilities/parameters.md b/docs/utilities/parameters.md index 280641e8a93..d30f87bf25e 100644 --- a/docs/utilities/parameters.md +++ b/docs/utilities/parameters.md @@ -102,7 +102,6 @@ You can set a parameter using the `set_parameter` high-level function. This will --8<-- "examples/parameters/src/getting_started_set_ssm_parameter_overwrite.py" ``` - ### Fetching secrets You can fetch secrets stored in Secrets Manager using `get_secret`. @@ -121,7 +120,6 @@ You can set secrets stored in Secrets Manager using `set_secret`. --8<-- "examples/parameters/src/getting_started_setting_secret.py" ``` - ### Fetching app configurations You can fetch application configurations in AWS AppConfig using `get_app_config`. diff --git a/tests/functional/test_utilities_parameters.py b/tests/functional/test_utilities_parameters.py index 44ddc33e2c1..30b6359bf8e 100644 --- a/tests/functional/test_utilities_parameters.py +++ b/tests/functional/test_utilities_parameters.py @@ -526,15 +526,16 @@ def test_ssm_provider_set(mock_name, mock_value, mock_version, config): "Value": mock_value, "Type": "String", "Overwrite": False, + "Description": "", "Tier": "Standard", } stubber.add_response("put_parameter", response, expected_params) stubber.activate() try: - version = provider.set(mock_name, mock_value) + version = provider.set(name=mock_name, value=mock_value) - assert version == mock_version + assert version == response stubber.assert_no_pending_responses() finally: stubber.deactivate() @@ -545,6 +546,8 @@ def test_ssm_provider_set_default_config(monkeypatch, mock_name, mock_value, moc Test SSMProvider._set() without specifying the config """ + mock_value = "leo" + monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-2") # Create a new provider @@ -556,17 +559,60 @@ def test_ssm_provider_set_default_config(monkeypatch, mock_name, mock_value, moc expected_params = { "Name": mock_name, "Value": mock_value, - "Type": "SecureString", + "Type": "String", "Overwrite": False, + "Tier": "Standard", + "Description": "", + } + stubber.add_response("put_parameter", response, expected_params) + stubber.activate() + + try: + version = provider.set(name=mock_name, value=mock_value) + + assert version == response + stubber.assert_no_pending_responses() + finally: + stubber.deactivate() + + +def test_ssm_provider_set_with_custom_options(monkeypatch, mock_name, mock_value, mock_version): + """ + Test SSMProvider._set() without specifying the config + """ + + mock_value = "leo" + + monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-2") + + # Create a new provider + provider = parameters.SSMProvider() + + # Stub the boto3 client + stubber = stub.Stubber(provider.client) + response = {"Version": mock_version, "Tier": "Advanced"} + expected_params = { + "Name": mock_name, + "Value": mock_value, + "Type": "SecureString", + "Overwrite": True, "Tier": "Advanced", + "Description": "Parameter", } stubber.add_response("put_parameter", response, expected_params) stubber.activate() try: - version = provider._set(mock_name, mock_value) + version = provider.set( + name=mock_name, + value=mock_value, + tier="Advanced", + parameter_type="SecureString", + overwrite=True, + description="Parameter", + ) - assert version == mock_version + assert version == response stubber.assert_no_pending_responses() finally: stubber.deactivate() From 3e79eacd2b41b626d4f583132714062f7c716934 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Wed, 20 Mar 2024 18:26:34 +0000 Subject: [PATCH 54/67] Adding correct exception for Secrets + fixing examples --- aws_lambda_powertools/utilities/parameters/exceptions.py | 6 +++++- aws_lambda_powertools/utilities/parameters/secrets.py | 7 +++---- .../src/getting_started_set_single_ssm_parameter.py | 2 +- .../src/getting_started_set_ssm_parameter_overwrite.py | 6 +++++- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/exceptions.py b/aws_lambda_powertools/utilities/parameters/exceptions.py index 087954b43a3..32364980df5 100644 --- a/aws_lambda_powertools/utilities/parameters/exceptions.py +++ b/aws_lambda_powertools/utilities/parameters/exceptions.py @@ -12,4 +12,8 @@ class TransformParameterError(Exception): class SetParameterError(Exception): - """When a provider raises an exception on setting a parameter""" + """When a provider raises an exception on setting a SSM parameter""" + + +class SetSecretError(Exception): + """When a provider raises an exception on setting an AWS Secret""" diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index f67d3390f62..1016e961252 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -19,9 +19,8 @@ from aws_lambda_powertools.shared import constants from aws_lambda_powertools.shared.functions import resolve_max_age - -from .base import DEFAULT_MAX_AGE_SECS, DEFAULT_PROVIDERS, BaseProvider -from .exceptions import SetParameterError +from aws_lambda_powertools.utilities.parameters.base import DEFAULT_MAX_AGE_SECS, DEFAULT_PROVIDERS, BaseProvider +from aws_lambda_powertools.utilities.parameters.exceptions import SetSecretError class SecretsProvider(BaseProvider): @@ -179,7 +178,7 @@ def _set( return value["VersionId"] except ClientError as exc: if exc.response["Error"]["Code"] != "ResourceNotFoundException": - raise SetParameterError(str(exc)) from exc + raise SetSecretError(str(exc)) from exc @overload diff --git a/examples/parameters/src/getting_started_set_single_ssm_parameter.py b/examples/parameters/src/getting_started_set_single_ssm_parameter.py index 64e86ca78a9..4718d99105f 100644 --- a/examples/parameters/src/getting_started_set_single_ssm_parameter.py +++ b/examples/parameters/src/getting_started_set_single_ssm_parameter.py @@ -5,7 +5,7 @@ def lambda_handler(event: dict, context: LambdaContext) -> dict: try: # Set a single parameter, returns the version ID of the parameter - parameter_version = parameters.set_parameter(path="/mySuper/Parameter", value="PowerToolsIsAwesome") # type: ignore[assignment] # noqa: E501 + parameter_version = parameters.set_parameter(name="/mySuper/Parameter", value="PowerToolsIsAwesome") return {"mySuperParameterVersion": parameter_version, "statusCode": 200} except parameters.exceptions.SetParameterError as error: diff --git a/examples/parameters/src/getting_started_set_ssm_parameter_overwrite.py b/examples/parameters/src/getting_started_set_ssm_parameter_overwrite.py index 53ef035c3b4..3217845caa0 100644 --- a/examples/parameters/src/getting_started_set_ssm_parameter_overwrite.py +++ b/examples/parameters/src/getting_started_set_ssm_parameter_overwrite.py @@ -6,7 +6,11 @@ def lambda_handler(event: dict, context: LambdaContext) -> dict: try: # Set a single parameter, but overwrite if it already exists # by default, overwrite is False, explicitly set it to True - updating_parameter = parameters.set_parameter(name="/mySuper/Parameter", value="PowerToolsIsAwesome", overwrite=True) # type: ignore[assignment] # noqa: E501 + updating_parameter = parameters.set_parameter( + name="/mySuper/Parameter", + value="PowerToolsIsAwesome", + overwrite=True, + ) return {"mySuperParameterVersion": updating_parameter, "statusCode": 200} except parameters.exceptions.SetParameterError as error: From 401cdf810ce6ed323c6176e1576d4b5e76c06627 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Wed, 20 Mar 2024 18:28:29 +0000 Subject: [PATCH 55/67] Making SonarCloud happy --- tests/functional/test_utilities_parameters.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/functional/test_utilities_parameters.py b/tests/functional/test_utilities_parameters.py index 30b6359bf8e..a7b1a731399 100644 --- a/tests/functional/test_utilities_parameters.py +++ b/tests/functional/test_utilities_parameters.py @@ -545,9 +545,6 @@ def test_ssm_provider_set_default_config(monkeypatch, mock_name, mock_value, moc """ Test SSMProvider._set() without specifying the config """ - - mock_value = "leo" - monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-2") # Create a new provider @@ -581,8 +578,6 @@ def test_ssm_provider_set_with_custom_options(monkeypatch, mock_name, mock_value Test SSMProvider._set() without specifying the config """ - mock_value = "leo" - monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-2") # Create a new provider From 44710baa2bc975d116d7ef4feb5f1abbb155203a Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Wed, 20 Mar 2024 18:49:36 +0000 Subject: [PATCH 56/67] Changing return from setSecret + fix mypy issues --- .../utilities/parameters/secrets.py | 13 +++---------- aws_lambda_powertools/utilities/parameters/types.py | 9 ++++++++- .../src/getting_started_setting_secret.py | 4 +++- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index 1016e961252..70862ab7e3b 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -10,9 +10,8 @@ import boto3 from botocore.config import Config -from botocore.exceptions import ClientError -from aws_lambda_powertools.utilities.parameters.types import TransformOptions +from aws_lambda_powertools.utilities.parameters.types import SetSecretResponse, TransformOptions if TYPE_CHECKING: from mypy_boto3_secretsmanager import SecretsManagerClient @@ -20,7 +19,6 @@ from aws_lambda_powertools.shared import constants from aws_lambda_powertools.shared.functions import resolve_max_age from aws_lambda_powertools.utilities.parameters.base import DEFAULT_MAX_AGE_SECS, DEFAULT_PROVIDERS, BaseProvider -from aws_lambda_powertools.utilities.parameters.exceptions import SetSecretError class SecretsProvider(BaseProvider): @@ -129,7 +127,7 @@ def _set( client_request_token: Optional[str] = None, version_stages: Optional[list[str]] = None, **sdk_options, - ) -> str: + ) -> SetSecretResponse: """ Modifies the details of a secret, including metadata and the secret value. @@ -173,12 +171,7 @@ def _set( if client_request_token: sdk_options["ClientRequestToken"] = client_request_token - try: - value = self.client.put_secret_value(**sdk_options) - return value["VersionId"] - except ClientError as exc: - if exc.response["Error"]["Code"] != "ResourceNotFoundException": - raise SetSecretError(str(exc)) from exc + return self.client.put_secret_value(**sdk_options) @overload diff --git a/aws_lambda_powertools/utilities/parameters/types.py b/aws_lambda_powertools/utilities/parameters/types.py index a9b0e702aff..707f5f42c07 100644 --- a/aws_lambda_powertools/utilities/parameters/types.py +++ b/aws_lambda_powertools/utilities/parameters/types.py @@ -1,4 +1,4 @@ -from aws_lambda_powertools.shared.types import Literal, TypedDict +from aws_lambda_powertools.shared.types import List, Literal, TypedDict TransformOptions = Literal["json", "binary", "auto", None] @@ -7,3 +7,10 @@ class PutParameterResponse(TypedDict): Version: int Tier: str ResponseMetadata: dict + + +class SetSecretResponse(TypedDict): + ARN: str + Name: str + VersionId: str + VersionStages: List[str] diff --git a/examples/parameters/src/getting_started_setting_secret.py b/examples/parameters/src/getting_started_setting_secret.py index b1f6c9d23de..ffa7fa7b7ff 100644 --- a/examples/parameters/src/getting_started_setting_secret.py +++ b/examples/parameters/src/getting_started_setting_secret.py @@ -6,10 +6,12 @@ logger = Logger(serialize_stacktrace=True) + def access_token(client_id: str, client_secret: str, audience: str) -> str: # example function that returns a JWT Access Token ... - return access_token + return f"{client_id}.{client_secret}.{audience}" + def lambda_handler(event: dict, context: LambdaContext): try: From 31777b1777971333889803eceaaa4b6bbf74f40e Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Wed, 20 Mar 2024 21:00:53 +0000 Subject: [PATCH 57/67] Increasing coverage --- .../utilities/parameters/secrets.py | 6 +++- .../utilities/parameters/ssm.py | 7 ++-- tests/functional/test_utilities_parameters.py | 33 +++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index 70862ab7e3b..3162b11ff78 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -19,6 +19,7 @@ from aws_lambda_powertools.shared import constants from aws_lambda_powertools.shared.functions import resolve_max_age from aws_lambda_powertools.utilities.parameters.base import DEFAULT_MAX_AGE_SECS, DEFAULT_PROVIDERS, BaseProvider +from aws_lambda_powertools.utilities.parameters.exceptions import SetSecretError class SecretsProvider(BaseProvider): @@ -171,7 +172,10 @@ def _set( if client_request_token: sdk_options["ClientRequestToken"] = client_request_token - return self.client.put_secret_value(**sdk_options) + try: + return self.client.put_secret_value(**sdk_options) + except Exception as exc: + raise SetSecretError(f"Error setting secret - {str(exc)}") from exc @overload diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index 2f21b1e1756..b67706b05d8 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -24,7 +24,7 @@ BaseProvider, transform_value, ) -from aws_lambda_powertools.utilities.parameters.exceptions import GetParameterError +from aws_lambda_powertools.utilities.parameters.exceptions import GetParameterError, SetParameterError from aws_lambda_powertools.utilities.parameters.types import PutParameterResponse, TransformOptions if TYPE_CHECKING: @@ -246,7 +246,10 @@ def set( if kms_key_id: opts["KeyId"] = kms_key_id - return self.client.put_parameter(**opts) + try: + return self.client.put_parameter(**opts) + except Exception as exc: + raise SetParameterError(f"Error setting parameter - {str(exc)}") from exc def _get(self, name: str, decrypt: bool = False, **sdk_options) -> str: """ diff --git a/tests/functional/test_utilities_parameters.py b/tests/functional/test_utilities_parameters.py index a7b1a731399..9eb1665c54e 100644 --- a/tests/functional/test_utilities_parameters.py +++ b/tests/functional/test_utilities_parameters.py @@ -593,6 +593,7 @@ def test_ssm_provider_set_with_custom_options(monkeypatch, mock_name, mock_value "Overwrite": True, "Tier": "Advanced", "Description": "Parameter", + "KeyId": "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", } stubber.add_response("put_parameter", response, expected_params) stubber.activate() @@ -605,6 +606,7 @@ def test_ssm_provider_set_with_custom_options(monkeypatch, mock_name, mock_value parameter_type="SecureString", overwrite=True, description="Parameter", + kms_key_id="arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", ) assert version == response @@ -613,6 +615,37 @@ def test_ssm_provider_set_with_custom_options(monkeypatch, mock_name, mock_value stubber.deactivate() +def test_ssm_provider_set_raise_on_failure(mock_name, mock_value, mock_version, config): + """ + Test SSMProvider.set_parameter() with failure + """ + # Create a new provider + provider = parameters.SSMProvider(config=config) + + # Stub the boto3 client + stubber = stub.Stubber(provider.client) + response = {"Version": mock_version, "Tier": "Standard"} + expected_params = { + "Name": mock_name, + "Value": mock_value, + "Type": "String", + "Overwrite": False, + "Description": "", + "Tier": "NoTier", + } + stubber.add_response("put_parameter", response, expected_params) + stubber.activate() + + # WHEN cannot set a Parameter with tier=NoTier + # THEN raise SetParameterError + with pytest.raises(parameters.exceptions.SetParameterError, match="Error setting parameter*"): + try: + provider.set(name=mock_name, value=mock_value) + stubber.assert_no_pending_responses() + finally: + stubber.deactivate() + + def test_ssm_provider_get_with_custom_client(mock_name, mock_value, mock_version, config): """ Test SSMProvider.get() with a non-cached value From c016d24005d34ea7f6da72f18ae47877df436068 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Wed, 20 Mar 2024 21:29:46 +0000 Subject: [PATCH 58/67] Increasing coverage --- .../utilities/parameters/types.py | 1 + tests/functional/test_utilities_parameters.py | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/aws_lambda_powertools/utilities/parameters/types.py b/aws_lambda_powertools/utilities/parameters/types.py index 707f5f42c07..f02ff75d69f 100644 --- a/aws_lambda_powertools/utilities/parameters/types.py +++ b/aws_lambda_powertools/utilities/parameters/types.py @@ -14,3 +14,4 @@ class SetSecretResponse(TypedDict): Name: str VersionId: str VersionStages: List[str] + ResponseMetadata: dict diff --git a/tests/functional/test_utilities_parameters.py b/tests/functional/test_utilities_parameters.py index 9eb1665c54e..f22f179534e 100644 --- a/tests/functional/test_utilities_parameters.py +++ b/tests/functional/test_utilities_parameters.py @@ -511,6 +511,29 @@ def test_ssm_provider_get(mock_name, mock_value, mock_version, config): stubber.deactivate() +def test_set_parameter(monkeypatch, mock_name, mock_value): + """ + Test get_parameter() + """ + + class TestProvider(BaseProvider): + def set(self, name: str, value: Any, *, overwrite: bool = False, **kwargs) -> str: + assert name == mock_name + return mock_value + + def _get(self, name: str, **kwargs) -> str: + raise NotImplementedError() + + def _get_multiple(self, path: str, **kwargs) -> Dict[str, str]: + raise NotImplementedError() + + monkeypatch.setitem(parameters.base.DEFAULT_PROVIDERS, "ssm", TestProvider()) + + value = parameters.set_parameter(name=mock_name, value=mock_value) + + assert value == mock_value + + def test_ssm_provider_set(mock_name, mock_value, mock_version, config): """ Test SSMProvider.set_parameter() with a non-cached value From 3bf45dff4909252dd83624d69d8540a22c82466d Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Wed, 20 Mar 2024 22:28:43 +0000 Subject: [PATCH 59/67] Refactoring secrets --- .../utilities/parameters/secrets.py | 27 ++++++++------ .../utilities/parameters/types.py | 7 ++-- docs/utilities/parameters.md | 4 +-- .../src/getting_started_setting_secret.py | 4 +-- tests/functional/test_utilities_parameters.py | 35 +++++++++++++++---- 5 files changed, 54 insertions(+), 23 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index 3162b11ff78..fdcb76aac9d 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -120,13 +120,23 @@ def _get_multiple(self, path: str, **sdk_options) -> Dict[str, str]: """ raise NotImplementedError() - def _set( + def _create_secret(self, name: str, **sdk_options): + try: + sdk_options["Name"] = name + return self.client.create_secret(**sdk_options) + except Exception as exc: + raise SetSecretError(f"Error setting secret - {str(exc)}") from exc + + def _update_secret(self, name: str, **sdk_options): + sdk_options["SecretId"] = name + return self.client.put_secret_value(**sdk_options) + + def set( self, name: str, value: Union[str, dict, bytes], *, # force keyword arguments client_request_token: Optional[str] = None, - version_stages: Optional[list[str]] = None, **sdk_options, ) -> SetSecretResponse: """ @@ -143,8 +153,6 @@ def _set( a UUID-type value to ensure uniqueness within the specified secret. This value becomes the VersionId of the new version. This field is autopopulated if not provided. - version_stages: list[str], optional - Specifies a list of staging labels that are attached to this version of the secret. sdk_options: dict, optional Dictionary of options that will be passed to the Secrets Manager update_secret API call @@ -157,8 +165,6 @@ def _set( https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager/client/put_secret_value.html """ - sdk_options["SecretId"] = name - if isinstance(value, dict): value = json.dumps(value) @@ -167,13 +173,13 @@ def _set( else: sdk_options["SecretString"] = value - if version_stages: - sdk_options["VersionStages"] = version_stages if client_request_token: sdk_options["ClientRequestToken"] = client_request_token try: - return self.client.put_secret_value(**sdk_options) + return self._update_secret(name=name, **sdk_options) + except self.client.exceptions.ResourceNotFoundException: + return self._create_secret(name=name, **sdk_options) except Exception as exc: raise SetSecretError(f"Error setting secret - {str(exc)}") from exc @@ -350,10 +356,9 @@ def set_secret( if "secrets" not in DEFAULT_PROVIDERS: DEFAULT_PROVIDERS["secrets"] = SecretsProvider() - return DEFAULT_PROVIDERS["secrets"]._set( + return DEFAULT_PROVIDERS["secrets"].set( name=name, value=value, client_request_token=client_request_token, - version_stages=version_stages, **sdk_options, ) diff --git a/aws_lambda_powertools/utilities/parameters/types.py b/aws_lambda_powertools/utilities/parameters/types.py index f02ff75d69f..c087a3764f4 100644 --- a/aws_lambda_powertools/utilities/parameters/types.py +++ b/aws_lambda_powertools/utilities/parameters/types.py @@ -1,4 +1,6 @@ -from aws_lambda_powertools.shared.types import List, Literal, TypedDict +from typing import Any, Optional + +from aws_lambda_powertools.shared.types import Dict, List, Literal, TypedDict TransformOptions = Literal["json", "binary", "auto", None] @@ -13,5 +15,6 @@ class SetSecretResponse(TypedDict): ARN: str Name: str VersionId: str - VersionStages: List[str] + VersionStages: Optional[List[str]] + ReplicationStatus: Optional[List[Dict[str, Any]]] ResponseMetadata: dict diff --git a/docs/utilities/parameters.md b/docs/utilities/parameters.md index d30f87bf25e..0d7f4be67e9 100644 --- a/docs/utilities/parameters.md +++ b/docs/utilities/parameters.md @@ -32,10 +32,10 @@ This utility requires additional permissions to work as expected. | SSM | **`get_parameter`**, **`SSMProvider.get`** | **`ssm:GetParameter`** | | SSM | **`get_parameters`**, **`SSMProvider.get_multiple`** | **`ssm:GetParametersByPath`** | | SSM | **`get_parameters_by_name`**, **`SSMProvider.get_parameters_by_name`** | **`ssm:GetParameter`** and **`ssm:GetParameters`** | -| SSM | **`set_parameter`** | **`ssm:PutParameter`** | +| SSM | **`set_parameter`**, **`SSMProvider.set_parameter`** | **`ssm:PutParameter`** | | SSM | If using **`decrypt=True`** | You must add an additional permission **`kms:Decrypt`** | | Secrets | **`get_secret`**, **`SecretsProvider.get`** | **`secretsmanager:GetSecretValue`** | -| Secrets | **`set_secret`**, **`SecretsProvider.get`** | **`secretsmanager:PutSecretValue`** and or **`secretsmanager:CreateSecret`** | +| Secrets | **`set_secret`**, **`SecretsProvider.set`** | **`secretsmanager:PutSecretValue`** and or **`secretsmanager:CreateSecret`** | | DynamoDB | **`DynamoDBProvider.get`** | **`dynamodb:GetItem`** | | DynamoDB | **`DynamoDBProvider.get_multiple`** | **`dynamodb:Query`** | | AppConfig | **`get_app_config`**, **`AppConfigProvider.get_app_config`** | **`appconfig:GetLatestConfiguration`** and **`appconfig:StartConfigurationSession`** | diff --git a/examples/parameters/src/getting_started_setting_secret.py b/examples/parameters/src/getting_started_setting_secret.py index ffa7fa7b7ff..50412380fdf 100644 --- a/examples/parameters/src/getting_started_setting_secret.py +++ b/examples/parameters/src/getting_started_setting_secret.py @@ -9,7 +9,7 @@ def access_token(client_id: str, client_secret: str, audience: str) -> str: # example function that returns a JWT Access Token - ... + # add your own logic here return f"{client_id}.{client_secret}.{audience}" @@ -25,6 +25,6 @@ def lambda_handler(event: dict, context: LambdaContext): update_secret_version_id = parameters.set_secret(name="/aws-powertools/jwt_token", value=jwt_token) return {"access_token": "updated", "statusCode": 200, "update_secret_version_id": update_secret_version_id} - except parameters.exceptions.SetParameterError as error: + except parameters.exceptions.SetSecretError as error: logger.exception(error) return {"access_token": "updated", "statusCode": 400} diff --git a/tests/functional/test_utilities_parameters.py b/tests/functional/test_utilities_parameters.py index f22f179534e..72f66affaf8 100644 --- a/tests/functional/test_utilities_parameters.py +++ b/tests/functional/test_utilities_parameters.py @@ -513,7 +513,7 @@ def test_ssm_provider_get(mock_name, mock_value, mock_version, config): def test_set_parameter(monkeypatch, mock_name, mock_value): """ - Test get_parameter() + Test set_parameter() """ class TestProvider(BaseProvider): @@ -534,7 +534,7 @@ def _get_multiple(self, path: str, **kwargs) -> Dict[str, str]: assert value == mock_value -def test_ssm_provider_set(mock_name, mock_value, mock_version, config): +def test_ssm_provider_set_parameter(mock_name, mock_value, mock_version, config): """ Test SSMProvider.set_parameter() with a non-cached value """ @@ -564,7 +564,7 @@ def test_ssm_provider_set(mock_name, mock_value, mock_version, config): stubber.deactivate() -def test_ssm_provider_set_default_config(monkeypatch, mock_name, mock_value, mock_version): +def test_ssm_provider_set_parameter_default_config(monkeypatch, mock_name, mock_value, mock_version): """ Test SSMProvider._set() without specifying the config """ @@ -596,9 +596,9 @@ def test_ssm_provider_set_default_config(monkeypatch, mock_name, mock_value, moc stubber.deactivate() -def test_ssm_provider_set_with_custom_options(monkeypatch, mock_name, mock_value, mock_version): +def test_ssm_provider_set_parameter_with_custom_options(monkeypatch, mock_name, mock_value, mock_version): """ - Test SSMProvider._set() without specifying the config + Test SSMProvider._set() with custom options """ monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-2") @@ -638,7 +638,7 @@ def test_ssm_provider_set_with_custom_options(monkeypatch, mock_name, mock_value stubber.deactivate() -def test_ssm_provider_set_raise_on_failure(mock_name, mock_value, mock_version, config): +def test_ssm_provider_set_parameter_raise_on_failure(mock_name, mock_value, mock_version, config): """ Test SSMProvider.set_parameter() with failure """ @@ -669,6 +669,29 @@ def test_ssm_provider_set_raise_on_failure(mock_name, mock_value, mock_version, stubber.deactivate() +def test_set_secret(monkeypatch, mock_name, mock_value): + """ + Test set_secret() + """ + + class TestProvider(BaseProvider): + def set(self, name: str, value: Any, *, overwrite: bool = False, **kwargs) -> str: + assert name == mock_name + return mock_value + + def _get(self, name: str, **kwargs) -> str: + raise NotImplementedError() + + def _get_multiple(self, path: str, **kwargs) -> Dict[str, str]: + raise NotImplementedError() + + monkeypatch.setitem(parameters.base.DEFAULT_PROVIDERS, "secrets", TestProvider()) + + value = parameters.set_secret(name=mock_name, value=mock_value) + + assert value == mock_value + + def test_ssm_provider_get_with_custom_client(mock_name, mock_value, mock_version, config): """ Test SSMProvider.get() with a non-cached value From 57eb44b81d16a469176d5471eae001fcc2c46892 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Thu, 21 Mar 2024 11:20:43 +0000 Subject: [PATCH 60/67] Improving docstring --- .../utilities/parameters/secrets.py | 116 +++++++++++++++--- .../utilities/parameters/ssm.py | 54 +++++++- 2 files changed, 148 insertions(+), 22 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index fdcb76aac9d..6bb9dbb7218 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -121,6 +121,19 @@ def _get_multiple(self, path: str, **sdk_options) -> Dict[str, str]: raise NotImplementedError() def _create_secret(self, name: str, **sdk_options): + """ + Create a secret with the given name. + + Parameters: + ---------- + name: str + The name of the secret. + **sdk_options: + Additional options to be passed to the create_secret method. + + Raises: + SetSecretError: If there is an error setting the secret. + """ try: sdk_options["Name"] = name return self.client.create_secret(**sdk_options) @@ -128,6 +141,16 @@ def _create_secret(self, name: str, **sdk_options): raise SetSecretError(f"Error setting secret - {str(exc)}") from exc def _update_secret(self, name: str, **sdk_options): + """ + Update a secret with the given name. + + Parameters: + ---------- + name: str + The name of the secret. + **sdk_options: + Additional options to be passed to the create_secret method. + """ sdk_options["SecretId"] = name return self.client.put_secret_value(**sdk_options) @@ -140,13 +163,28 @@ def set( **sdk_options, ) -> SetSecretResponse: """ - Modifies the details of a secret, including metadata and the secret value. + Modify the details of a secret or create a new secret if it doesn't already exist. + It includes metadata and the secret value. + + We aim to minimize API calls by assuming that the secret already exists and needs updating. + If it doesn't exist, we attempt to create a new one. Refer to the following workflow for a better understanding: + + + ┌────────────────────────┐ ┌─────────────────┐ + ┌───────▶│Resource NotFound error?│────▶│Create Secret API│─────┐ + │ └────────────────────────┘ └─────────────────┘ │ + │ │ + │ │ + │ ▼ + ┌─────────────────┐ ┌─────────────────────┐ + │Update Secret API│────────────────────────────────────────────▶│ Return or Exception │ + └─────────────────┘ └─────────────────────┘ Parameters ---------- name: str - The ARN or name of the secret to add a new version to. - value: str or bytes + The ARN or name of the secret to add a new version to or create a new one. + value: str, dict or bytes Specifies text data that you want to encrypt and store in this new version of the secret. client_request_token: str, optional This value helps ensure idempotency. Recommended that you generate @@ -156,13 +194,39 @@ def set( sdk_options: dict, optional Dictionary of options that will be passed to the Secrets Manager update_secret API call + Raises + ------ + SetSecretError + When attempting to update or create a secret fails. + Returns: ------- - Version ID of the newly created version of the secret. + SetSecretResponse: + The dict returned by boto3. + + Example + ------- + **Sets a secret*** + + >>> from aws_lambda_powertools.utilities import parameters + >>> + >>> parameters.set_secret(name="llamas-are-awesome", value="supers3cr3tllam@passw0rd") + + **Sets a secret and includes an client_request_token** + + >>> from aws_lambda_powertools.utilities import parameters + >>> import uuid + >>> + >>> parameters.set_secret( + name="my-secret", + value='{"password": "supers3cr3tllam@passw0rd"}', + client_request_token=str(uuid.uuid4()) + ) URLs: ------- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager/client/put_secret_value.html + https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager/client/create_secret.html """ if isinstance(value, dict): @@ -301,37 +365,46 @@ def set_secret( **sdk_options, ) -> str: """ - Retrieve a parameter value from AWS Secrets Manager + Modify the details of a secret or create a new secret if it doesn't already exist. + It includes metadata and the secret value. + + We aim to minimize API calls by assuming that the secret already exists and needs updating. + If it doesn't exist, we attempt to create a new one. Refer to the following workflow for a better understanding: + + + ┌────────────────────────┐ ┌─────────────────┐ + ┌───────▶│Resource NotFound error?│────▶│Create Secret API│─────┐ + │ └────────────────────────┘ └─────────────────┘ │ + │ │ + │ │ + │ ▼ + ┌─────────────────┐ ┌─────────────────────┐ + │Update Secret API│────────────────────────────────────────────▶│ Return or Exception │ + └─────────────────┘ └─────────────────────┘ Parameters ---------- name: str - Name of the parameter - value: str or bytes - Secret value to set + The ARN or name of the secret to add a new version to or create a new one. + value: str, dict or bytes + Specifies text data that you want to encrypt and store in this new version of the secret. client_request_token: str, optional This value helps ensure idempotency. Recommended that you generate a UUID-type value to ensure uniqueness within the specified secret. This value becomes the VersionId of the new version. This field is autopopulated if not provided. - version_stages: list[str], optional - A list of staging labels that are attached to this version of the secret. sdk_options: dict, optional - Dictionary of options that will be passed to the get_secret_value call + Dictionary of options that will be passed to the Secrets Manager update_secret API call Raises ------ - SetParameterError - When the secrets provider fails to set a secret value or secret binary for - a given name. + SetSecretError + When attempting to update or create a secret fails. Returns: ------- - Version ID of the newly created version of the secret. - - URLs: - ------- - https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager/client/put_secret_value.html + SetSecretResponse: + The dict returned by boto3. Example ------- @@ -350,6 +423,11 @@ def set_secret( value='{"password": "supers3cr3tllam@passw0rd"}', client_request_token="61f2af5f-5f75-44b1-a29f-0cc37af55b11" ) + + URLs: + ------- + https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager/client/put_secret_value.html + https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager/client/create_secret.html """ # Only create the provider if this function is called at least once diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index b67706b05d8..350bd047213 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -233,6 +233,54 @@ def set( kms_key_id: str | None = None, **sdk_options, ) -> PutParameterResponse: + """ + Sets a parameter in AWS Systems Manager Parameter Store. + + Parameters + ---------- + name: str + The fully qualified name includes the complete hierarchy of the parameter name and name. + value: str + The parameter value + parameter_type: str, optional + Type of the parameter. Allowed values are String, StringList, and SecureString + overwrite: bool, optional + If the parameter value should be overwritten, False by default + tier: str, optional + The parameter tier to use. Allowed values are Standard, Advanced, and Intelligent-Tiering + description: str, optional + The description of the parameter + kms_key_id: str, optional + The KMS key id to use to encrypt the parameter + sdk_options: dict, optional + Dictionary of options that will be passed to the Parameter Store get_parameter API call + + Raises + ------ + SetParameterError + When the parameter provider fails to retrieve a parameter value for + a given name. + + URLs: + ------- + https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ssm/client/put_parameter.html + + Example + ------- + **Sets a parameter value from Systems Manager Parameter Store** + + >>> from aws_lambda_powertools.utilities import parameters + >>> + >>> response = parameters.set_parameter(name="/my/example/parameter", value="More Powertools") + >>> + >>> print(response) + 123 + + Returns + ------- + PutParameterResponse + The dict returned by boto3. + """ opts = { "Name": name, "Value": value, @@ -929,8 +977,7 @@ def set_parameter( Raises ------ SetParameterError - When the parameter provider fails to retrieve a parameter value for - a given name. + When attempting to set a parameter fails. URLs: ------- @@ -949,7 +996,8 @@ def set_parameter( Returns ------- - int: The status code indicating the success or failure of the operation. + PutParameterResponse + The dict returned by boto3. """ # Only create the provider if this function is called at least once From df0ffc98d5f3c51a0cbb249492fe0d56a8718d21 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Thu, 21 Mar 2024 13:44:21 +0000 Subject: [PATCH 61/67] Adding more tests --- .../utilities/parameters/secrets.py | 4 +- tests/functional/test_utilities_parameters.py | 159 +++++++++++++++++- 2 files changed, 155 insertions(+), 8 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index 6bb9dbb7218..ff673525b01 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -190,7 +190,7 @@ def set( This value helps ensure idempotency. Recommended that you generate a UUID-type value to ensure uniqueness within the specified secret. This value becomes the VersionId of the new version. This field is - autopopulated if not provided. + auto-populated if not provided. sdk_options: dict, optional Dictionary of options that will be passed to the Secrets Manager update_secret API call @@ -392,7 +392,7 @@ def set_secret( This value helps ensure idempotency. Recommended that you generate a UUID-type value to ensure uniqueness within the specified secret. This value becomes the VersionId of the new version. This field is - autopopulated if not provided. + auto-populated if not provided. sdk_options: dict, optional Dictionary of options that will be passed to the Secrets Manager update_secret API call diff --git a/tests/functional/test_utilities_parameters.py b/tests/functional/test_utilities_parameters.py index 72f66affaf8..e1ebc8c01fa 100644 --- a/tests/functional/test_utilities_parameters.py +++ b/tests/functional/test_utilities_parameters.py @@ -4,6 +4,7 @@ import json import random import string +import uuid from datetime import datetime, timedelta from io import BytesIO from typing import Any, Dict, List, Tuple @@ -556,9 +557,7 @@ def test_ssm_provider_set_parameter(mock_name, mock_value, mock_version, config) stubber.activate() try: - version = provider.set(name=mock_name, value=mock_value) - - assert version == response + assert provider.set(name=mock_name, value=mock_value) == response stubber.assert_no_pending_responses() finally: stubber.deactivate() @@ -588,9 +587,7 @@ def test_ssm_provider_set_parameter_default_config(monkeypatch, mock_name, mock_ stubber.activate() try: - version = provider.set(name=mock_name, value=mock_value) - - assert version == response + assert provider.set(name=mock_name, value=mock_value) == response stubber.assert_no_pending_responses() finally: stubber.deactivate() @@ -674,6 +671,7 @@ def test_set_secret(monkeypatch, mock_name, mock_value): Test set_secret() """ + # GIVEN a mock implementation of BaseProvider set method class TestProvider(BaseProvider): def set(self, name: str, value: Any, *, overwrite: bool = False, **kwargs) -> str: assert name == mock_name @@ -687,11 +685,160 @@ def _get_multiple(self, path: str, **kwargs) -> Dict[str, str]: monkeypatch.setitem(parameters.base.DEFAULT_PROVIDERS, "secrets", TestProvider()) + # WHEN set_secret function is called value = parameters.set_secret(name=mock_name, value=mock_value) + # THEN it should return the mock_value assert value == mock_value +def test_secret_provider_update_secret_with_plain_text_value(mock_name, mock_value, config): + """ + Test SecretsProvider.set() with a plain text value + """ + # GIVEN a SecretsProvider instance + provider = parameters.SecretsProvider(config=config) + + client_request_token = str(uuid.uuid4()) + + # WHEN setting a secret with a plain text value + stubber = stub.Stubber(provider.client) + response = {"Name": mock_name, "ARN": f"arn:aws:secretsmanager:us-east-1:132456789012:secret/{mock_name}"} + expected_params = { + "SecretId": mock_name, + "SecretString": mock_value, + "ClientRequestToken": client_request_token, + } + stubber.add_response("put_secret_value", response, expected_params) + stubber.activate() + + # THEN it should call put_secret_value with the plain text value and the client request token + try: + assert response == provider.set(name=mock_name, value=mock_value, client_request_token=client_request_token) + stubber.assert_no_pending_responses() + finally: + stubber.deactivate() + + +def test_secret_provider_update_secret_with_binary_value(mock_name, config): + """ + Test SecretsProvider.set() with a binary value + """ + + mock_value = b"value_to_test" + + # GIVEN a SecretsProvider instance + provider = parameters.SecretsProvider(config=config) + + # WHEN setting a secret with a binary value + stubber = stub.Stubber(provider.client) + response = {"Name": mock_name, "ARN": f"arn:aws:secretsmanager:us-east-1:132456789012:secret/{mock_name}"} + expected_params = { + "SecretId": mock_name, + "SecretBinary": mock_value, + } + stubber.add_response("put_secret_value", response, expected_params) + stubber.activate() + + # THEN it should call put_secret_value with the binary value + try: + assert response == provider.set(name=mock_name, value=mock_value) + stubber.assert_no_pending_responses() + finally: + stubber.deactivate() + + +def test_secret_provider_update_secret_with_dict_value(mock_name, config): + """ + Test SecretsProvider.set() with a dict value + """ + + mock_value = {"key": "powertools"} + + # GIVEN a SecretsProvider instance + provider = parameters.SecretsProvider(config=config) + + # WHEN setting a secret with a dictionary value + stubber = stub.Stubber(provider.client) + response = {"Name": mock_name, "ARN": f"arn:aws:secretsmanager:us-east-1:132456789012:secret/{mock_name}"} + expected_params = { + "SecretId": mock_name, + "SecretString": json.dumps(mock_value), + } + stubber.add_response("put_secret_value", response, expected_params) + stubber.activate() + + # THEN it should encode the dictionary as JSON and call put_secret_value with the encoded value + try: + assert response == provider.set(name=mock_name, value=mock_value) + stubber.assert_no_pending_responses() + finally: + stubber.deactivate() + + +def test_secret_provider_update_secret_with_raise_on_failure(mock_name, mock_value, config): + """ + Test SecretsProvider.set() with raise on failure + """ + # GIVEN a SecretsProvider instance + provider = parameters.SecretsProvider(config=config) + + # Stub the boto3 client + stubber = stub.Stubber(provider.client) + response = {"Name": mock_name, "ARN": f"arn:aws:secretsmanager:us-east-1:132456789012:secret/{mock_name}"} + expected_params = { + "SecretName": mock_name, + "SecretString": mock_value, + } + stubber.add_response("put_secret_value", response, expected_params) + stubber.activate() + + # WHEN cannot update a Secret with wrong parameter + # THEN raise SetSecretError + with pytest.raises(parameters.exceptions.SetSecretError, match="Error setting secret*"): + try: + assert response == provider.set(name=mock_name, value=mock_value) + stubber.assert_no_pending_responses() + finally: + stubber.deactivate() + + +def test_secret_provider_create_secret(mocker, mock_name, mock_value, config): + """ + Test Test SecretsProvider.set() forcing a new secret creation + """ + # GIVEN a SecretsProvider instance + provider = parameters.SecretsProvider(config=config) + + # WHEN the put_secret_value method raises a ResourceNotFoundException + mock_update_secret = mocker.patch.object(provider, "_update_secret") + mock_update_secret.side_effect = provider.client.exceptions.ResourceNotFoundException( + {"Error": {"Code": "ResourceNotFoundException"}}, + "put_secret_value", + ) + + # WHEN setting values for a new secret + client_request_token = str(uuid.uuid4()) + # Stub the boto3 client + stubber = stub.Stubber(provider.client) + response = {"Name": mock_name, "ARN": f"arn:aws:secretsmanager:us-east-1:132456789012:secret/{mock_name}"} + expected_params = { + "Name": mock_name, + "SecretString": mock_value, + "ClientRequestToken": client_request_token, + } + + # THEN it should call create_secret + stubber.add_response("create_secret", response, expected_params) + stubber.activate() + + try: + assert response == provider.set(name=mock_name, value=mock_value, client_request_token=client_request_token) + stubber.assert_no_pending_responses() + finally: + stubber.deactivate() + + def test_ssm_provider_get_with_custom_client(mock_name, mock_value, mock_version, config): """ Test SSMProvider.get() with a non-cached value From 0efe6d7e37687aa2ba44f150e25bdbc3b7d1d66b Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Thu, 21 Mar 2024 13:45:16 +0000 Subject: [PATCH 62/67] Making SonarCloud happy --- aws_lambda_powertools/utilities/parameters/secrets.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index ff673525b01..56125599610 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -361,7 +361,6 @@ def set_secret( value: Union[str, bytes], *, # force keyword arguments client_request_token: Optional[str] = None, - version_stages: Optional[list[str]] = None, **sdk_options, ) -> str: """ From 6fae9ddba32967399e294daeaa002c99dec55ab2 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Thu, 21 Mar 2024 14:04:42 +0000 Subject: [PATCH 63/67] Adding more tests and docs --- .../utilities/parameters/secrets.py | 5 ++ docs/utilities/parameters.md | 5 +- tests/functional/test_utilities_parameters.py | 54 ++++++++++++++++--- 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index 56125599610..d8f6aefc519 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -5,6 +5,7 @@ from __future__ import annotations import json +import logging import os from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Union, overload @@ -21,6 +22,8 @@ from aws_lambda_powertools.utilities.parameters.base import DEFAULT_MAX_AGE_SECS, DEFAULT_PROVIDERS, BaseProvider from aws_lambda_powertools.utilities.parameters.exceptions import SetSecretError +logger = logging.getLogger(__name__) + class SecretsProvider(BaseProvider): """ @@ -241,8 +244,10 @@ def set( sdk_options["ClientRequestToken"] = client_request_token try: + logger.debug(f"Attempting to update secret {name}") return self._update_secret(name=name, **sdk_options) except self.client.exceptions.ResourceNotFoundException: + logger.debug(f"Secret {name} doesn't exist, creating a new one") return self._create_secret(name=name, **sdk_options) except Exception as exc: raise SetSecretError(f"Error setting secret - {str(exc)}") from exc diff --git a/docs/utilities/parameters.md b/docs/utilities/parameters.md index 0d7f4be67e9..c04294164e8 100644 --- a/docs/utilities/parameters.md +++ b/docs/utilities/parameters.md @@ -115,8 +115,11 @@ You can fetch secrets stored in Secrets Manager using `get_secret`. You can set secrets stored in Secrets Manager using `set_secret`. +???+ note + We strive to minimize API calls by attempting to update existing secrets as our primary approach. If a secret doesn't exist, we proceed to create a new one. + === "getting_started_secret.py" - ```python hl_lines="5 15" + ```python hl_lines="4 25" --8<-- "examples/parameters/src/getting_started_setting_secret.py" ``` diff --git a/tests/functional/test_utilities_parameters.py b/tests/functional/test_utilities_parameters.py index e1ebc8c01fa..334b3d37ea5 100644 --- a/tests/functional/test_utilities_parameters.py +++ b/tests/functional/test_utilities_parameters.py @@ -539,10 +539,10 @@ def test_ssm_provider_set_parameter(mock_name, mock_value, mock_version, config) """ Test SSMProvider.set_parameter() with a non-cached value """ - # Create a new provider + # GIVEN a SSMProvider instance with default values provider = parameters.SSMProvider(config=config) - # Stub the boto3 client + # WHEN setting a parameter stubber = stub.Stubber(provider.client) response = {"Version": mock_version, "Tier": "Standard"} expected_params = { @@ -556,6 +556,7 @@ def test_ssm_provider_set_parameter(mock_name, mock_value, mock_version, config) stubber.add_response("put_parameter", response, expected_params) stubber.activate() + # THEN it should return values try: assert provider.set(name=mock_name, value=mock_value) == response stubber.assert_no_pending_responses() @@ -569,10 +570,10 @@ def test_ssm_provider_set_parameter_default_config(monkeypatch, mock_name, mock_ """ monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-2") - # Create a new provider + # GIVEN a SSMProvider instance with default values provider = parameters.SSMProvider() - # Stub the boto3 client + # WHEN setting a parameter stubber = stub.Stubber(provider.client) response = {"Version": mock_version, "Tier": "Advanced"} expected_params = { @@ -586,6 +587,7 @@ def test_ssm_provider_set_parameter_default_config(monkeypatch, mock_name, mock_ stubber.add_response("put_parameter", response, expected_params) stubber.activate() + # THEN it should return values try: assert provider.set(name=mock_name, value=mock_value) == response stubber.assert_no_pending_responses() @@ -600,10 +602,10 @@ def test_ssm_provider_set_parameter_with_custom_options(monkeypatch, mock_name, monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-2") - # Create a new provider + # GIVEN a SSMProvider instance provider = parameters.SSMProvider() - # Stub the boto3 client + # WHEN using custom parameters stubber = stub.Stubber(provider.client) response = {"Version": mock_version, "Tier": "Advanced"} expected_params = { @@ -618,6 +620,7 @@ def test_ssm_provider_set_parameter_with_custom_options(monkeypatch, mock_name, stubber.add_response("put_parameter", response, expected_params) stubber.activate() + # THEN it should return values try: version = provider.set( name=mock_name, @@ -639,7 +642,7 @@ def test_ssm_provider_set_parameter_raise_on_failure(mock_name, mock_value, mock """ Test SSMProvider.set_parameter() with failure """ - # Create a new provider + # GIVEN a SSMProvider instance provider = parameters.SSMProvider(config=config) # Stub the boto3 client @@ -839,6 +842,43 @@ def test_secret_provider_create_secret(mocker, mock_name, mock_value, config): stubber.deactivate() +def test_secret_provider_create_secret_raise_on_error(mocker, mock_name, mock_value, config): + """ + Test Test SecretsProvider.set() forcing a new secret creation + """ + # GIVEN a SecretsProvider instance + provider = parameters.SecretsProvider(config=config) + + # WHEN the put_secret_value method raises a ResourceNotFoundException + mock_update_secret = mocker.patch.object(provider, "_update_secret") + mock_update_secret.side_effect = provider.client.exceptions.ResourceNotFoundException( + {"Error": {"Code": "ResourceNotFoundException"}}, + "put_secret_value", + ) + + # WHEN setting values for a new secret with wrong parameters + client_request_token = str(uuid.uuid4()) + # Stub the boto3 client + stubber = stub.Stubber(provider.client) + response = {"Name": mock_name, "ARN": f"arn:aws:secretsmanager:us-east-1:132456789012:secret/{mock_name}"} + expected_params = { + "NameSecret": mock_name, + "SecretString": mock_value, + "ClientRequestToken": client_request_token, + } + stubber.add_response("create_secret", response, expected_params) + stubber.activate() + + # WHEN cannot update a Secret with wrong parameter + # THEN raise SetSecretError + with pytest.raises(parameters.exceptions.SetSecretError, match="Error setting secret*"): + try: + assert response == provider.set(name=mock_name, value=mock_value) + stubber.assert_no_pending_responses() + finally: + stubber.deactivate() + + def test_ssm_provider_get_with_custom_client(mock_name, mock_value, mock_version, config): """ Test SSMProvider.get() with a non-cached value From f65570f29d82317772dd34a6d43054a58cdaa686 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Thu, 21 Mar 2024 15:19:56 +0000 Subject: [PATCH 64/67] Addressing Ruben's feedback --- .../utilities/parameters/exceptions.py | 4 ++-- .../utilities/parameters/secrets.py | 18 ++++++++---------- .../utilities/parameters/ssm.py | 16 ++++++++-------- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/exceptions.py b/aws_lambda_powertools/utilities/parameters/exceptions.py index 32364980df5..6a9554bf142 100644 --- a/aws_lambda_powertools/utilities/parameters/exceptions.py +++ b/aws_lambda_powertools/utilities/parameters/exceptions.py @@ -12,8 +12,8 @@ class TransformParameterError(Exception): class SetParameterError(Exception): - """When a provider raises an exception on setting a SSM parameter""" + """When a provider raises an exception on writing a SSM parameter""" class SetSecretError(Exception): - """When a provider raises an exception on setting an AWS Secret""" + """When a provider raises an exception on writing a secret""" diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index d8f6aefc519..0494c64985a 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -12,15 +12,15 @@ import boto3 from botocore.config import Config -from aws_lambda_powertools.utilities.parameters.types import SetSecretResponse, TransformOptions - if TYPE_CHECKING: from mypy_boto3_secretsmanager import SecretsManagerClient from aws_lambda_powertools.shared import constants from aws_lambda_powertools.shared.functions import resolve_max_age +from aws_lambda_powertools.shared.json_encoder import Encoder from aws_lambda_powertools.utilities.parameters.base import DEFAULT_MAX_AGE_SECS, DEFAULT_PROVIDERS, BaseProvider from aws_lambda_powertools.utilities.parameters.exceptions import SetSecretError +from aws_lambda_powertools.utilities.parameters.types import SetSecretResponse, TransformOptions logger = logging.getLogger(__name__) @@ -167,7 +167,6 @@ def set( ) -> SetSecretResponse: """ Modify the details of a secret or create a new secret if it doesn't already exist. - It includes metadata and the secret value. We aim to minimize API calls by assuming that the secret already exists and needs updating. If it doesn't exist, we attempt to create a new one. Refer to the following workflow for a better understanding: @@ -190,10 +189,10 @@ def set( value: str, dict or bytes Specifies text data that you want to encrypt and store in this new version of the secret. client_request_token: str, optional - This value helps ensure idempotency. Recommended that you generate + This value helps ensure idempotency. It's recommended that you generate a UUID-type value to ensure uniqueness within the specified secret. This value becomes the VersionId of the new version. This field is - auto-populated if not provided. + auto-populated if not provided, but no idempotency will be enforced this way. sdk_options: dict, optional Dictionary of options that will be passed to the Secrets Manager update_secret API call @@ -233,7 +232,7 @@ def set( """ if isinstance(value, dict): - value = json.dumps(value) + value = json.dumps(value, cls=Encoder) if isinstance(value, bytes): sdk_options["SecretBinary"] = value @@ -367,10 +366,9 @@ def set_secret( *, # force keyword arguments client_request_token: Optional[str] = None, **sdk_options, -) -> str: +) -> SetSecretResponse: """ Modify the details of a secret or create a new secret if it doesn't already exist. - It includes metadata and the secret value. We aim to minimize API calls by assuming that the secret already exists and needs updating. If it doesn't exist, we attempt to create a new one. Refer to the following workflow for a better understanding: @@ -393,10 +391,10 @@ def set_secret( value: str, dict or bytes Specifies text data that you want to encrypt and store in this new version of the secret. client_request_token: str, optional - This value helps ensure idempotency. Recommended that you generate + This value helps ensure idempotency. It's recommended that you generate a UUID-type value to ensure uniqueness within the specified secret. This value becomes the VersionId of the new version. This field is - auto-populated if not provided. + auto-populated if not provided, but no idempotency will be enforced this way. sdk_options: dict, optional Dictionary of options that will be passed to the Secrets Manager update_secret API call diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index 350bd047213..76553bda0fe 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -242,14 +242,14 @@ def set( The fully qualified name includes the complete hierarchy of the parameter name and name. value: str The parameter value - parameter_type: str, optional - Type of the parameter. Allowed values are String, StringList, and SecureString overwrite: bool, optional If the parameter value should be overwritten, False by default - tier: str, optional - The parameter tier to use. Allowed values are Standard, Advanced, and Intelligent-Tiering description: str, optional The description of the parameter + parameter_type: str, optional + Type of the parameter. Allowed values are String, StringList, and SecureString + tier: str, optional + The parameter tier to use. Allowed values are Standard, Advanced, and Intelligent-Tiering kms_key_id: str, optional The KMS key id to use to encrypt the parameter sdk_options: dict, optional @@ -961,14 +961,14 @@ def set_parameter( The fully qualified name includes the complete hierarchy of the parameter name and name. value: str The parameter value - parameter_type: str, optional - Type of the parameter. Allowed values are String, StringList, and SecureString overwrite: bool, optional If the parameter value should be overwritten, False by default - tier: str, optional - The parameter tier to use. Allowed values are Standard, Advanced, and Intelligent-Tiering description: str, optional The description of the parameter + parameter_type: str, optional + Type of the parameter. Allowed values are String, StringList, and SecureString + tier: str, optional + The parameter tier to use. Allowed values are Standard, Advanced, and Intelligent-Tiering kms_key_id: str, optional The KMS key id to use to encrypt the parameter sdk_options: dict, optional From bf5f05baa4804ac610be245933bc6b525fbebd37 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Thu, 21 Mar 2024 15:25:18 +0000 Subject: [PATCH 65/67] Addressing Ruben's feedback --- .../src/getting_started_set_ssm_parameter_overwrite.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/parameters/src/getting_started_set_ssm_parameter_overwrite.py b/examples/parameters/src/getting_started_set_ssm_parameter_overwrite.py index 3217845caa0..a80cf2d9818 100644 --- a/examples/parameters/src/getting_started_set_ssm_parameter_overwrite.py +++ b/examples/parameters/src/getting_started_set_ssm_parameter_overwrite.py @@ -4,8 +4,8 @@ def lambda_handler(event: dict, context: LambdaContext) -> dict: try: - # Set a single parameter, but overwrite if it already exists - # by default, overwrite is False, explicitly set it to True + # Set a single parameter, but overwrite if it already exists. + # Overwrite is False by default, so we explicitly set it to True updating_parameter = parameters.set_parameter( name="/mySuper/Parameter", value="PowerToolsIsAwesome", From 4cebf7507320b57bafbca97caae2c0e5c72f8230 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Thu, 21 Mar 2024 15:31:13 +0000 Subject: [PATCH 66/67] Addressing Ruben's feedback --- aws_lambda_powertools/utilities/parameters/base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aws_lambda_powertools/utilities/parameters/base.py b/aws_lambda_powertools/utilities/parameters/base.py index 1446646c55e..2317ebc82d9 100644 --- a/aws_lambda_powertools/utilities/parameters/base.py +++ b/aws_lambda_powertools/utilities/parameters/base.py @@ -155,6 +155,9 @@ def _get(self, name: str, **sdk_options) -> Union[str, bytes, Dict[str, Any]]: raise NotImplementedError() def set(self, name: str, value: Any, *, overwrite: bool = False, **kwargs): + """ + Set parameter value from the underlying parameter store + """ raise NotImplementedError() def get_multiple( From 273c544dd509d58d6bd43268ad3780da968c400a Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Fri, 22 Mar 2024 10:02:51 +0000 Subject: [PATCH 67/67] Addressing Ruben's feedback --- docs/utilities/parameters.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/utilities/parameters.md b/docs/utilities/parameters.md index c04294164e8..92c0c53ce86 100644 --- a/docs/utilities/parameters.md +++ b/docs/utilities/parameters.md @@ -27,18 +27,18 @@ This utility requires additional permissions to work as expected. ???+ note Different parameter providers require different permissions. -| Provider | Function/Method | IAM Permission | -| --------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | -| SSM | **`get_parameter`**, **`SSMProvider.get`** | **`ssm:GetParameter`** | -| SSM | **`get_parameters`**, **`SSMProvider.get_multiple`** | **`ssm:GetParametersByPath`** | -| SSM | **`get_parameters_by_name`**, **`SSMProvider.get_parameters_by_name`** | **`ssm:GetParameter`** and **`ssm:GetParameters`** | -| SSM | **`set_parameter`**, **`SSMProvider.set_parameter`** | **`ssm:PutParameter`** | -| SSM | If using **`decrypt=True`** | You must add an additional permission **`kms:Decrypt`** | -| Secrets | **`get_secret`**, **`SecretsProvider.get`** | **`secretsmanager:GetSecretValue`** | -| Secrets | **`set_secret`**, **`SecretsProvider.set`** | **`secretsmanager:PutSecretValue`** and or **`secretsmanager:CreateSecret`** | -| DynamoDB | **`DynamoDBProvider.get`** | **`dynamodb:GetItem`** | -| DynamoDB | **`DynamoDBProvider.get_multiple`** | **`dynamodb:Query`** | -| AppConfig | **`get_app_config`**, **`AppConfigProvider.get_app_config`** | **`appconfig:GetLatestConfiguration`** and **`appconfig:StartConfigurationSession`** | +| Provider | Function/Method | IAM Permission | +| --------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| SSM | **`get_parameter`**, **`SSMProvider.get`** | **`ssm:GetParameter`** | +| SSM | **`get_parameters`**, **`SSMProvider.get_multiple`** | **`ssm:GetParametersByPath`** | +| SSM | **`get_parameters_by_name`**, **`SSMProvider.get_parameters_by_name`** | **`ssm:GetParameter`** and **`ssm:GetParameters`** | +| SSM | **`set_parameter`**, **`SSMProvider.set_parameter`** | **`ssm:PutParameter`** | +| SSM | If using **`decrypt=True`** | You must add an additional permission **`kms:Decrypt`** | +| Secrets | **`get_secret`**, **`SecretsProvider.get`** | **`secretsmanager:GetSecretValue`** | +| Secrets | **`set_secret`**, **`SecretsProvider.set`** | **`secretsmanager:PutSecretValue`** and **`secretsmanager:CreateSecret`** (if creating secrets) | +| DynamoDB | **`DynamoDBProvider.get`** | **`dynamodb:GetItem`** | +| DynamoDB | **`DynamoDBProvider.get_multiple`** | **`dynamodb:Query`** | +| AppConfig | **`get_app_config`**, **`AppConfigProvider.get_app_config`** | **`appconfig:GetLatestConfiguration`** and **`appconfig:StartConfigurationSession`** | ### Fetching parameters @@ -96,9 +96,9 @@ You can set a parameter using the `set_parameter` high-level function. This will ``` === "getting_started_set_ssm_parameter_overwrite.py" - There are occasions where sometimes you are setting a parameter and then you may need to update that parameter later on. In this case, you can use the `overwrite` parameter to overwrite the parameter value if it already exists. If you do not set this parameter, then the parameter will not be overwritten and an exception will be raised. + Sometimes you may be setting a parameter that you will have to update later on. Use the `overwrite` option to overwrite any existing value. If you do not set this option, the parameter value will not be overwritten and an exception will be raised. - ```python hl_lines="8" + ```python hl_lines="8 12" --8<-- "examples/parameters/src/getting_started_set_ssm_parameter_overwrite.py" ```