Skip to content

Commit 43eac11

Browse files
stephenbawksheitorlessaleandrodamascenaaradyaronsthulb
authored
feat(parameters): add feature for creating and updating Parameters and Secrets (#2858)
* gotta start somewhere * small tweaks * adding set param * adding exception classes * adding set parameter * fixing set * few more updates * missing value * adding documentation * one more update * question about transform yet * remove optional transform * updates * updating put secret value * fixing value on return secret * cleaning up naming and examples * fix example and return type * update * fix a few new warnings * simplying secret value * small tweaks * cleaning up * missed one * cleaning up ruff * couple of modifications * forgot to remove this here * fix * adding create when not existing * fix name * create flag * Update aws_lambda_powertools/utilities/parameters/secrets.py Co-authored-by: aradyaron <[email protected]> Signed-off-by: Stephen Bawks <[email protected]> * couple refinements * updating docstrings * 💄 fix example again * 📝 documentation is seriously tough * adding examples and documentation for set_param * trying to add some docs * creating "realistic" example * updating example * making example more appropiate * missed one * couple tests so far * changing variable name * removing the create logic for the time being * remove create option * chore: remove set as mandatory method (temporarily) Signed-off-by: heitorlessa <[email protected]> * fix(parameters): make cache aware of single vs multiple calls Signed-off-by: heitorlessa <[email protected]> * chore: cleanup, add test for single and nested Signed-off-by: heitorlessa <[email protected]> * refactor: implement set() minimum contract, and set() in ssm Signed-off-by: heitorlessa <[email protected]> * refactor: use name over path in set_parameter Signed-off-by: heitorlessa <[email protected]> * Adding docstring * Fixing description type + adding TypeDict for set_parameter * Refactoring tests * Adding correct exception for Secrets + fixing examples * Making SonarCloud happy * Changing return from setSecret + fix mypy issues * Increasing coverage * Increasing coverage * Refactoring secrets * Improving docstring * Adding more tests * Making SonarCloud happy * Adding more tests and docs * Addressing Ruben's feedback * Addressing Ruben's feedback * Addressing Ruben's feedback * Addressing Ruben's feedback --------- Signed-off-by: Stephen Bawks <[email protected]> Signed-off-by: heitorlessa <[email protected]> Co-authored-by: Heitor Lessa <[email protected]> Co-authored-by: Leandro Damascena <[email protected]> Co-authored-by: aradyaron <[email protected]> Co-authored-by: Simon Thulbourn <[email protected]> Co-authored-by: heitorlessa <[email protected]>
1 parent 49fc491 commit 43eac11

File tree

11 files changed

+933
-20
lines changed

11 files changed

+933
-20
lines changed

aws_lambda_powertools/utilities/parameters/__init__.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
from .base import BaseProvider, clear_caches
99
from .dynamodb import DynamoDBProvider
1010
from .exceptions import GetParameterError, TransformParameterError
11-
from .secrets import SecretsProvider, get_secret
12-
from .ssm import SSMProvider, get_parameter, get_parameters, get_parameters_by_name
11+
from .secrets import SecretsProvider, get_secret, set_secret
12+
from .ssm import SSMProvider, get_parameter, get_parameters, get_parameters_by_name, set_parameter
1313

1414
__all__ = [
1515
"AppConfigProvider",
@@ -21,8 +21,10 @@
2121
"TransformParameterError",
2222
"get_app_config",
2323
"get_parameter",
24+
"set_parameter",
2425
"get_parameters",
2526
"get_parameters_by_name",
2627
"get_secret",
28+
"set_secret",
2729
"clear_caches",
2830
]

aws_lambda_powertools/utilities/parameters/base.py

+6
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,12 @@ def _get(self, name: str, **sdk_options) -> Union[str, bytes, Dict[str, Any]]:
154154
"""
155155
raise NotImplementedError()
156156

157+
def set(self, name: str, value: Any, *, overwrite: bool = False, **kwargs):
158+
"""
159+
Set parameter value from the underlying parameter store
160+
"""
161+
raise NotImplementedError()
162+
157163
def get_multiple(
158164
self,
159165
path: str,

aws_lambda_powertools/utilities/parameters/exceptions.py

+8
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,11 @@ class GetParameterError(Exception):
99

1010
class TransformParameterError(Exception):
1111
"""When a provider fails to transform a parameter value"""
12+
13+
14+
class SetParameterError(Exception):
15+
"""When a provider raises an exception on writing a SSM parameter"""
16+
17+
18+
class SetSecretError(Exception):
19+
"""When a provider raises an exception on writing a secret"""

aws_lambda_powertools/utilities/parameters/secrets.py

+221-3
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,27 @@
22
AWS Secrets Manager parameter retrieval and caching utility
33
"""
44

5+
from __future__ import annotations
6+
7+
import json
8+
import logging
59
import os
610
from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Union, overload
711

812
import boto3
913
from botocore.config import Config
1014

11-
from aws_lambda_powertools.utilities.parameters.types import TransformOptions
12-
1315
if TYPE_CHECKING:
1416
from mypy_boto3_secretsmanager import SecretsManagerClient
1517

1618
from aws_lambda_powertools.shared import constants
1719
from aws_lambda_powertools.shared.functions import resolve_max_age
20+
from aws_lambda_powertools.shared.json_encoder import Encoder
21+
from aws_lambda_powertools.utilities.parameters.base import DEFAULT_MAX_AGE_SECS, DEFAULT_PROVIDERS, BaseProvider
22+
from aws_lambda_powertools.utilities.parameters.exceptions import SetSecretError
23+
from aws_lambda_powertools.utilities.parameters.types import SetSecretResponse, TransformOptions
1824

19-
from .base import DEFAULT_MAX_AGE_SECS, DEFAULT_PROVIDERS, BaseProvider
25+
logger = logging.getLogger(__name__)
2026

2127

2228
class SecretsProvider(BaseProvider):
@@ -117,6 +123,134 @@ def _get_multiple(self, path: str, **sdk_options) -> Dict[str, str]:
117123
"""
118124
raise NotImplementedError()
119125

126+
def _create_secret(self, name: str, **sdk_options):
127+
"""
128+
Create a secret with the given name.
129+
130+
Parameters:
131+
----------
132+
name: str
133+
The name of the secret.
134+
**sdk_options:
135+
Additional options to be passed to the create_secret method.
136+
137+
Raises:
138+
SetSecretError: If there is an error setting the secret.
139+
"""
140+
try:
141+
sdk_options["Name"] = name
142+
return self.client.create_secret(**sdk_options)
143+
except Exception as exc:
144+
raise SetSecretError(f"Error setting secret - {str(exc)}") from exc
145+
146+
def _update_secret(self, name: str, **sdk_options):
147+
"""
148+
Update a secret with the given name.
149+
150+
Parameters:
151+
----------
152+
name: str
153+
The name of the secret.
154+
**sdk_options:
155+
Additional options to be passed to the create_secret method.
156+
"""
157+
sdk_options["SecretId"] = name
158+
return self.client.put_secret_value(**sdk_options)
159+
160+
def set(
161+
self,
162+
name: str,
163+
value: Union[str, dict, bytes],
164+
*, # force keyword arguments
165+
client_request_token: Optional[str] = None,
166+
**sdk_options,
167+
) -> SetSecretResponse:
168+
"""
169+
Modify the details of a secret or create a new secret if it doesn't already exist.
170+
171+
We aim to minimize API calls by assuming that the secret already exists and needs updating.
172+
If it doesn't exist, we attempt to create a new one. Refer to the following workflow for a better understanding:
173+
174+
175+
┌────────────────────────┐ ┌─────────────────┐
176+
┌───────▶│Resource NotFound error?│────▶│Create Secret API│─────┐
177+
│ └────────────────────────┘ └─────────────────┘ │
178+
│ │
179+
│ │
180+
│ ▼
181+
┌─────────────────┐ ┌─────────────────────┐
182+
│Update Secret API│────────────────────────────────────────────▶│ Return or Exception │
183+
└─────────────────┘ └─────────────────────┘
184+
185+
Parameters
186+
----------
187+
name: str
188+
The ARN or name of the secret to add a new version to or create a new one.
189+
value: str, dict or bytes
190+
Specifies text data that you want to encrypt and store in this new version of the secret.
191+
client_request_token: str, optional
192+
This value helps ensure idempotency. It's recommended that you generate
193+
a UUID-type value to ensure uniqueness within the specified secret.
194+
This value becomes the VersionId of the new version. This field is
195+
auto-populated if not provided, but no idempotency will be enforced this way.
196+
sdk_options: dict, optional
197+
Dictionary of options that will be passed to the Secrets Manager update_secret API call
198+
199+
Raises
200+
------
201+
SetSecretError
202+
When attempting to update or create a secret fails.
203+
204+
Returns:
205+
-------
206+
SetSecretResponse:
207+
The dict returned by boto3.
208+
209+
Example
210+
-------
211+
**Sets a secret***
212+
213+
>>> from aws_lambda_powertools.utilities import parameters
214+
>>>
215+
>>> parameters.set_secret(name="llamas-are-awesome", value="supers3cr3tllam@passw0rd")
216+
217+
**Sets a secret and includes an client_request_token**
218+
219+
>>> from aws_lambda_powertools.utilities import parameters
220+
>>> import uuid
221+
>>>
222+
>>> parameters.set_secret(
223+
name="my-secret",
224+
value='{"password": "supers3cr3tllam@passw0rd"}',
225+
client_request_token=str(uuid.uuid4())
226+
)
227+
228+
URLs:
229+
-------
230+
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager/client/put_secret_value.html
231+
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager/client/create_secret.html
232+
"""
233+
234+
if isinstance(value, dict):
235+
value = json.dumps(value, cls=Encoder)
236+
237+
if isinstance(value, bytes):
238+
sdk_options["SecretBinary"] = value
239+
else:
240+
sdk_options["SecretString"] = value
241+
242+
if client_request_token:
243+
sdk_options["ClientRequestToken"] = client_request_token
244+
245+
try:
246+
logger.debug(f"Attempting to update secret {name}")
247+
return self._update_secret(name=name, **sdk_options)
248+
except self.client.exceptions.ResourceNotFoundException:
249+
logger.debug(f"Secret {name} doesn't exist, creating a new one")
250+
return self._create_secret(name=name, **sdk_options)
251+
except Exception as exc:
252+
raise SetSecretError(f"Error setting secret - {str(exc)}") from exc
253+
120254

121255
@overload
122256
def get_secret(
@@ -224,3 +358,87 @@ def get_secret(
224358
force_fetch=force_fetch,
225359
**sdk_options,
226360
)
361+
362+
363+
def set_secret(
364+
name: str,
365+
value: Union[str, bytes],
366+
*, # force keyword arguments
367+
client_request_token: Optional[str] = None,
368+
**sdk_options,
369+
) -> SetSecretResponse:
370+
"""
371+
Modify the details of a secret or create a new secret if it doesn't already exist.
372+
373+
We aim to minimize API calls by assuming that the secret already exists and needs updating.
374+
If it doesn't exist, we attempt to create a new one. Refer to the following workflow for a better understanding:
375+
376+
377+
┌────────────────────────┐ ┌─────────────────┐
378+
┌───────▶│Resource NotFound error?│────▶│Create Secret API│─────┐
379+
│ └────────────────────────┘ └─────────────────┘ │
380+
│ │
381+
│ │
382+
│ ▼
383+
┌─────────────────┐ ┌─────────────────────┐
384+
│Update Secret API│────────────────────────────────────────────▶│ Return or Exception │
385+
└─────────────────┘ └─────────────────────┘
386+
387+
Parameters
388+
----------
389+
name: str
390+
The ARN or name of the secret to add a new version to or create a new one.
391+
value: str, dict or bytes
392+
Specifies text data that you want to encrypt and store in this new version of the secret.
393+
client_request_token: str, optional
394+
This value helps ensure idempotency. It's recommended that you generate
395+
a UUID-type value to ensure uniqueness within the specified secret.
396+
This value becomes the VersionId of the new version. This field is
397+
auto-populated if not provided, but no idempotency will be enforced this way.
398+
sdk_options: dict, optional
399+
Dictionary of options that will be passed to the Secrets Manager update_secret API call
400+
401+
Raises
402+
------
403+
SetSecretError
404+
When attempting to update or create a secret fails.
405+
406+
Returns:
407+
-------
408+
SetSecretResponse:
409+
The dict returned by boto3.
410+
411+
Example
412+
-------
413+
**Sets a secret***
414+
415+
>>> from aws_lambda_powertools.utilities import parameters
416+
>>>
417+
>>> parameters.set_secret(name="llamas-are-awesome", value="supers3cr3tllam@passw0rd")
418+
419+
**Sets a secret and includes an client_request_token**
420+
421+
>>> from aws_lambda_powertools.utilities import parameters
422+
>>>
423+
>>> parameters.set_secret(
424+
name="my-secret",
425+
value='{"password": "supers3cr3tllam@passw0rd"}',
426+
client_request_token="61f2af5f-5f75-44b1-a29f-0cc37af55b11"
427+
)
428+
429+
URLs:
430+
-------
431+
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager/client/put_secret_value.html
432+
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager/client/create_secret.html
433+
"""
434+
435+
# Only create the provider if this function is called at least once
436+
if "secrets" not in DEFAULT_PROVIDERS:
437+
DEFAULT_PROVIDERS["secrets"] = SecretsProvider()
438+
439+
return DEFAULT_PROVIDERS["secrets"].set(
440+
name=name,
441+
value=value,
442+
client_request_token=client_request_token,
443+
**sdk_options,
444+
)

0 commit comments

Comments
 (0)