Skip to content

Commit e1091f1

Browse files
author
Ran Isenberg
committed
feat: Add AppConfig parameter provider
1 parent 06d58d4 commit e1091f1

File tree

4 files changed

+223
-1
lines changed

4 files changed

+223
-1
lines changed

Diff for: aws_lambda_powertools/utilities/parameters/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,22 @@
44
Parameter retrieval and caching utility
55
"""
66

7+
from .appconf import AppConfigProvider, get_app_config
78
from .base import BaseProvider
89
from .dynamodb import DynamoDBProvider
910
from .exceptions import GetParameterError, TransformParameterError
1011
from .secrets import SecretsProvider, get_secret
1112
from .ssm import SSMProvider, get_parameter, get_parameters
1213

1314
__all__ = [
15+
"AppConfigProvider",
1416
"BaseProvider",
1517
"GetParameterError",
1618
"DynamoDBProvider",
1719
"SecretsProvider",
1820
"SSMProvider",
1921
"TransformParameterError",
22+
"get_app_config",
2023
"get_parameter",
2124
"get_parameters",
2225
"get_secret",
+185
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
"""
2+
AWS App Config configuration retrieval and caching utility
3+
"""
4+
5+
6+
import os
7+
from typing import Dict, Optional, Union
8+
from uuid import uuid4
9+
10+
import boto3
11+
from botocore.config import Config
12+
13+
from .base import DEFAULT_PROVIDERS, BaseProvider
14+
15+
CLIENT_ID = str(uuid4())
16+
17+
18+
class AppConfigProvider(BaseProvider):
19+
"""
20+
AWS App Config Provider
21+
22+
Parameters
23+
----------
24+
config: botocore.config.Config, optional
25+
Botocore configuration to pass during client initialization
26+
27+
Example
28+
-------
29+
**Retrieves a parameter value from App Config**
30+
31+
>>> from aws_lambda_powertools.utilities.parameters import AppConfigProvider
32+
>>> appconfig_provider = AppConfigProvider()
33+
>>>
34+
>>> value: Dict[str, str] = appconfig_provider.get("my-configuration")
35+
>>>
36+
>>> print(value)
37+
My configuration values
38+
39+
**Retrieves a parameter value from Systems Manager Parameter Store in another AWS region**
40+
41+
>>> from botocore.config import Config
42+
>>> from aws_lambda_powertools.utilities.parameters import SSMProvider
43+
>>>
44+
>>> config = Config(region_name="us-west-1")
45+
>>> ssm_provider = SSMProvider(config=config)
46+
>>>
47+
>>> value = ssm_provider.get("/my/parameter")
48+
>>>
49+
>>> print(value)
50+
My parameter value
51+
52+
**Retrieves multiple parameter values from Systems Manager Parameter Store using a path prefix**
53+
54+
>>> from aws_lambda_powertools.utilities.parameters import SSMProvider
55+
>>> ssm_provider = SSMProvider()
56+
>>>
57+
>>> values = ssm_provider.get_multiple("/my/path/prefix")
58+
>>>
59+
>>> for key, value in values.items():
60+
... print(key, value)
61+
/my/path/prefix/a Parameter value a
62+
/my/path/prefix/b Parameter value b
63+
/my/path/prefix/c Parameter value c
64+
65+
**Retrieves multiple parameter values from Systems Manager Parameter Store passing options to the SDK call**
66+
67+
>>> from aws_lambda_powertools.utilities.parameters import SSMProvider
68+
>>> ssm_provider = SSMProvider()
69+
>>>
70+
>>> values = ssm_provider.get_multiple("/my/path/prefix", MaxResults=10)
71+
>>>
72+
>>> for key, value in values.items():
73+
... print(key, value)
74+
/my/path/prefix/a Parameter value a
75+
/my/path/prefix/b Parameter value b
76+
/my/path/prefix/c Parameter value c
77+
"""
78+
79+
client = None
80+
81+
def __init__(
82+
self, environment: str, application: Optional[str] = None, config: Optional[Config] = None,
83+
):
84+
"""
85+
Initialize the SSM Parameter Store client
86+
"""
87+
88+
config = config or Config()
89+
self.client = boto3.client("appconfig", config=config)
90+
self.application = application or os.getenv("POWERTOOLS_SERVICE_NAME") or "application_undefined"
91+
self.environment = environment
92+
93+
super().__init__()
94+
95+
def _get(self, name: str, client_conf_version: Optional[str] = None, **sdk_options) -> str:
96+
"""
97+
Retrieve a parameter value from AWS Systems Manager Parameter Store
98+
99+
Parameters
100+
----------
101+
name: str
102+
Parameter name
103+
decrypt: bool, optional
104+
If the parameter value should be decrypted
105+
sdk_options: dict, optional
106+
Dictionary of options that will be passed to the Parameter Store get_parameter API call
107+
"""
108+
109+
sdk_options["Configuration"] = name
110+
sdk_options["Application"] = self.application
111+
sdk_options["Environment"] = self.environment
112+
sdk_options["ClientId"] = CLIENT_ID
113+
if client_conf_version is not None:
114+
sdk_options["ClientConfigurationVersion"] = client_conf_version
115+
116+
response = self.client.get_configuration(**sdk_options)
117+
return response["Content"].read() # read() of botocore.response.StreamingBody
118+
119+
def _get_multiple(self, path: str, **sdk_options) -> Dict[str, str]:
120+
"""
121+
Retrieving multiple parameter values is not supported with AWS Secrets Manager
122+
"""
123+
raise NotImplementedError()
124+
125+
126+
def get_app_config(
127+
name: str,
128+
environment: str,
129+
application: Optional[str] = None,
130+
transform: Optional[str] = None,
131+
client_conf_version: Optional[str] = None,
132+
**sdk_options
133+
) -> Union[str, list, dict, bytes]:
134+
"""
135+
Retrieve a parameter value from AWS Systems Manager (SSM) Parameter Store
136+
137+
Parameters
138+
----------
139+
name: str
140+
Name of the parameter
141+
transform: str, optional
142+
Transforms the content from a JSON object ('json') or base64 binary string ('binary')
143+
decrypt: bool, optional
144+
If the parameter values should be decrypted
145+
sdk_options: dict, optional
146+
Dictionary of options that will be passed to the Parameter Store get_parameter API call
147+
148+
Raises
149+
------
150+
GetParameterError
151+
When the parameter provider fails to retrieve a parameter value for
152+
a given name.
153+
TransformParameterError
154+
When the parameter provider fails to transform a parameter value.
155+
156+
Example
157+
-------
158+
**Retrieves a parameter value from Systems Manager Parameter Store**
159+
160+
>>> from aws_lambda_powertools.utilities.parameters import get_parameter
161+
>>>
162+
>>> value = get_parameter("/my/parameter")
163+
>>>
164+
>>> print(value)
165+
My parameter value
166+
167+
**Retrieves a parameter value and decodes it using a Base64 decoder**
168+
169+
>>> from aws_lambda_powertools.utilities.parameters import get_parameter
170+
>>>
171+
>>> value = get_parameter("/my/parameter", transform='binary')
172+
>>>
173+
>>> print(value)
174+
My parameter value
175+
"""
176+
177+
# Only create the provider if this function is called at least once
178+
if "appconfig" not in DEFAULT_PROVIDERS:
179+
DEFAULT_PROVIDERS["appconfig"] = AppConfigProvider(environment=environment, application=application)
180+
181+
sdk_options["ClientId"] = CLIENT_ID
182+
if client_conf_version is not None:
183+
sdk_options["ClientConfigurationVersion"] = client_conf_version
184+
185+
return DEFAULT_PROVIDERS["appconfig"].get(name, transform=transform, **sdk_options)

Diff for: aws_lambda_powertools/utilities/parameters/base.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
DEFAULT_PROVIDERS = {}
1818
TRANSFORM_METHOD_JSON = "json"
1919
TRANSFORM_METHOD_BINARY = "binary"
20-
SUPPORTED_TRANSFORM_METHODS = [TRANSFORM_METHOD_JSON, TRANSFORM_METHOD_BINARY]
20+
TRANSFORM_METHOD_YAML = "yaml"
21+
SUPPORTED_TRANSFORM_METHODS = [TRANSFORM_METHOD_JSON, TRANSFORM_METHOD_BINARY, TRANSFORM_METHOD_YAML]
2122

2223

2324
class BaseProvider(ABC):
@@ -230,6 +231,8 @@ def transform_value(value: str, transform: str, raise_on_transform_error: bool =
230231
return json.loads(value)
231232
elif transform == TRANSFORM_METHOD_BINARY:
232233
return base64.b64decode(value)
234+
elif transform == TRANSFORM_METHOD_YAML:
235+
return value # todo
233236
else:
234237
raise ValueError(f"Invalid transform type '{transform}'")
235238

Diff for: tests/functional/test_utilities_parameters.py

+31
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
import random
44
import string
55
from datetime import datetime, timedelta
6+
from io import BytesIO
67
from typing import Dict
78

89
import pytest
910
from boto3.dynamodb.conditions import Key
1011
from botocore import stub
1112
from botocore.config import Config
13+
from botocore.response import StreamingBody
1214

1315
from aws_lambda_powertools.utilities import parameters
1416
from aws_lambda_powertools.utilities.parameters.base import BaseProvider, ExpirableValue
@@ -1451,6 +1453,35 @@ def _get_multiple(self, path: str, **kwargs) -> Dict[str, str]:
14511453
assert value == mock_value
14521454

14531455

1456+
def test_appconf_provider_get_configuration_json_content_type(mock_name, mock_value, config):
1457+
"""
1458+
Test get_configuration.get with default values
1459+
"""
1460+
1461+
# Create a new provider
1462+
environment = "dev"
1463+
application = "myapp"
1464+
provider = parameters.AppConfigProvider(environment=environment, application=application)
1465+
1466+
mock_body_json = {"myenvvar1": "Black Panther", "myenvvar2": 3}
1467+
encoded_message = json.dumps(mock_body_json).encode("utf-8")
1468+
mock_value = StreamingBody(BytesIO(encoded_message), len(encoded_message))
1469+
1470+
# Stub the boto3 client
1471+
stubber = stub.Stubber(provider.client)
1472+
response = {"Content": mock_value, "ConfigurationVersion": "1", "ContentType": "application/json"}
1473+
stubber.add_response("get_configuration", response)
1474+
stubber.activate()
1475+
1476+
try:
1477+
value = provider.get(mock_name, client_conf_version="1", transform="json")
1478+
1479+
assert value == mock_body_json
1480+
stubber.assert_no_pending_responses()
1481+
finally:
1482+
stubber.deactivate()
1483+
1484+
14541485
def test_transform_value_json(mock_value):
14551486
"""
14561487
Test transform_value() with a json transform

0 commit comments

Comments
 (0)