Skip to content

feat: Add DynamoDB provider to parameters module #1091

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Mar 20, 2023
4 changes: 4 additions & 0 deletions powertools-parameters/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@
<groupId>software.amazon.awssdk</groupId>
<artifactId>url-connection-client</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>dynamodb</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package software.amazon.lambda.powertools.parameters;

import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
import software.amazon.awssdk.core.SdkSystemSetting;
import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.*;
import software.amazon.lambda.powertools.parameters.cache.CacheManager;
import software.amazon.lambda.powertools.parameters.transform.TransformationManager;

import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;

/**
* Implements a {@link ParamProvider} on top of DynamoDB. The schema of the table
* is described in the Python powertools documentation.
*
* @see <a href="https://awslabs.github.io/aws-lambda-powertools-python/latest/utilities/parameters/#dynamodbprovider">Python DynamoDB provider</a>
*
*/
public class DynamoDbProvider extends BaseProvider {

private final DynamoDbClient client;
private final String tableName;

public DynamoDbProvider(CacheManager cacheManager, String tableName) {
this(cacheManager, DynamoDbClient.builder()
.httpClientBuilder(UrlConnectionHttpClient.builder())
.credentialsProvider(EnvironmentVariableCredentialsProvider.create())
.region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable())))
.build(),
tableName
);

}

DynamoDbProvider(CacheManager cacheManager, DynamoDbClient client, String tableName) {
super(cacheManager);
this.client = client;
this.tableName = tableName;
}

/**
* Return a single value from the DynamoDB parameter provider.
*
* @param key key of the parameter
* @return The value, if it exists, null if it doesn't. Throws if the row exists but doesn't match the schema.
*/
@Override
protected String getValue(String key) {
GetItemResponse resp = client.getItem(GetItemRequest.builder()
.tableName(tableName)
.key(Collections.singletonMap("id", AttributeValue.fromS(key)))
.attributesToGet("val")
.build());

// If we have an item at the key, we should be able to get a 'val' out of it. If not it's
// exceptional.
// If we don't have an item at the key, we should return null.
if (resp.hasItem() && !resp.item().values().isEmpty()) {
return resp.item().get("val").s();
}

return null;
}

/**
* Returns multiple values from the DynamoDB parameter provider.
*
* @param path Parameter store path
* @return All values matching the given path, and an empty map if none do. Throws if any records exist that don't match the schema.
*/
@Override
protected Map<String, String> getMultipleValues(String path) {

QueryResponse resp = client.query(QueryRequest.builder()
.tableName(tableName)
.keyConditionExpression("id = :v_id")
.expressionAttributeValues(Collections.singletonMap(":v_id", AttributeValue.fromS(path)))
.build());

return resp
.items()
.stream()
.collect(
Collectors.toMap(
(i) -> i.get("sk").s(),
(i) -> i.get("val").s()));


}

/**
* Create a builder that can be used to configure and create a {@link DynamoDbProvider}.
*
* @return a new instance of {@link DynamoDbProvider.Builder}
*/
public static DynamoDbProvider.Builder builder() {
return new DynamoDbProvider.Builder();
}

static class Builder {
private DynamoDbClient client;
private String table;
private CacheManager cacheManager;
private TransformationManager transformationManager;

/**
* Create a {@link DynamoDbProvider} instance.
*
* @return a {@link DynamoDbProvider}
*/
public DynamoDbProvider build() {
if (cacheManager == null) {
throw new IllegalStateException("No CacheManager provided; please provide one");
}
if (table == null) {
throw new IllegalStateException("No DynamoDB table name provided; please provide one");
}
DynamoDbProvider provider;
if (client != null) {
provider = new DynamoDbProvider(cacheManager, client, table);
} else {
provider = new DynamoDbProvider(cacheManager, table);
}
if (transformationManager != null) {
provider.setTransformationManager(transformationManager);
}
return provider;
}

/**
* Set custom {@link DynamoDbClient} to pass to the {@link DynamoDbClient}. <br/>
* Use it if you want to customize the region or any other part of the client.
*
* @param client Custom client
* @return the builder to chain calls (eg. <pre>builder.withClient().build()</pre>)
*/
public DynamoDbProvider.Builder withClient(DynamoDbClient client) {
this.client = client;
return this;
}

/**
* <b>Mandatory</b>. Provide a CacheManager to the {@link DynamoDbProvider}
*
* @param cacheManager the manager that will handle the cache of parameters
* @return the builder to chain calls (eg. <pre>builder.withCacheManager().build()</pre>)
*/
public DynamoDbProvider.Builder withCacheManager(CacheManager cacheManager) {
this.cacheManager = cacheManager;
return this;
}

/**
* <b>Mandatory</b>. Provide a DynamoDB table to the {@link DynamoDbProvider}
*
* @param table the table that parameters will be retrieved from.
* @return the builder to chain calls (eg. <pre>builder.withTable().build()</pre>)
*/
public DynamoDbProvider.Builder withTable(String table) {
this.table = table;
return this;
}

/**
* Provide a transformationManager to the {@link DynamoDbProvider}
*
* @param transformationManager the manager that will handle transformation of parameters
* @return the builder to chain calls (eg. <pre>builder.withTransformationManager().build()</pre>)
*/
public DynamoDbProvider.Builder withTransformationManager(TransformationManager transformationManager) {
this.transformationManager = transformationManager;
return this;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.awssdk.services.ssm.SsmClient;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.lambda.powertools.parameters.cache.CacheManager;
import software.amazon.lambda.powertools.parameters.transform.TransformationManager;

Expand All @@ -36,8 +37,8 @@ public final class ParamManager {

/**
* Get a concrete implementation of {@link BaseProvider}.<br/>
* You can specify {@link SecretsProvider} or {@link SSMProvider} or create your custom provider
* by extending {@link BaseProvider} if you need to integrate with a different parameter store.
* You can specify {@link SecretsProvider}, {@link SSMProvider}, {@link DynamoDbProvider}, or create your
* custom provider by extending {@link BaseProvider} if you need to integrate with a different parameter store.
* @return a {@link SecretsProvider}
*/
public static <T extends BaseProvider> T getProvider(Class<T> providerClass) {
Expand Down Expand Up @@ -65,6 +66,12 @@ public static SSMProvider getSsmProvider() {
return getProvider(SSMProvider.class);
}

/**
* Get a {@link DynamoDbProvider} with default {@link DynamoDbClient} <br/>
* If you need to customize the region, or other part of the client, use
*/
public static DynamoDbProvider getDynamodbProvider() { return getProvider(DynamoDbProvider.class); }

/**
* Get a {@link SecretsProvider} with your custom {@link SecretsManagerClient}.<br/>
* Use this to configure region or other part of the client. Use {@link ParamManager#getSsmProvider()} if you don't need this customization.
Expand All @@ -91,6 +98,19 @@ public static SSMProvider getSsmProvider(SsmClient client) {
.build());
}

/**
* Get a {@link DynamoDbProvider} with your custom {@link DynamoDbClient}.<br/>
* Use this to configure region or other part of the client. Use {@link ParamManager#getDynamodbProvider()} if you don't need this customization.
* @return a {@link DynamoDbProvider}
*/
public static DynamoDbProvider getDynamoDbProvider(DynamoDbClient client) {
return (DynamoDbProvider) providers.computeIfAbsent(DynamoDbProvider.class, (k) -> DynamoDbProvider.builder()
.withClient(client)
.withCacheManager(cacheManager)
.withTransformationManager(transformationManager)
.build());
}

public static CacheManager getCacheManager() {
return cacheManager;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package software.amazon.lambda.powertools.parameters;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
import software.amazon.lambda.powertools.parameters.cache.CacheManager;

import java.util.HashMap;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;

/**
* This class provides simple end-to-end style testing of the DynamoDBProvider class.
* It is ignored, for now, as it requires AWS access and that's not yet run as part
* of our unit test suite in the cloud.
*
* The test is kept here for 1/ local development and 2/ in preparation for future
* E2E tests running in the cloud CI.
*/
@Disabled
public class DynamoDbProviderE2ETest {

final String ParamsTestTable = "ddb-params-test";
final String MultiparamsTestTable = "ddb-multiparams-test";
private final DynamoDbClient ddbClient;

public DynamoDbProviderE2ETest() {
// Create a DDB client to inject test data into our test tables
ddbClient = DynamoDbClient.builder()
.httpClientBuilder(UrlConnectionHttpClient.builder())
.build();


}

@Test
public void TestGetValue() {

// Arrange
HashMap<String, AttributeValue> testItem = new HashMap<String, AttributeValue>();
testItem.put("id", AttributeValue.fromS("test_param"));
testItem.put("val", AttributeValue.fromS("the_value_is_hello!"));
ddbClient.putItem(PutItemRequest.builder()
.tableName(ParamsTestTable)
.item(testItem)
.build());

// Act
DynamoDbProvider provider = makeProvider(ParamsTestTable);
String value = provider.getValue("test_param");

// Assert
assertThat(value).isEqualTo("the_value_is_hello!");
}

@Test
public void TestGetValues() {

// Arrange
HashMap<String, AttributeValue> testItem = new HashMap<String, AttributeValue>();
testItem.put("id", AttributeValue.fromS("test_param"));
testItem.put("sk", AttributeValue.fromS("test_param_part_1"));
testItem.put("val", AttributeValue.fromS("the_value_is_hello!"));
ddbClient.putItem(PutItemRequest.builder()
.tableName(MultiparamsTestTable)
.item(testItem)
.build());

HashMap<String, AttributeValue> testItem2 = new HashMap<String, AttributeValue>();
testItem2.put("id", AttributeValue.fromS("test_param"));
testItem2.put("sk", AttributeValue.fromS("test_param_part_2"));
testItem2.put("val", AttributeValue.fromS("the_value_is_still_hello!"));
ddbClient.putItem(PutItemRequest.builder()
.tableName(MultiparamsTestTable)
.item(testItem2)
.build());

// Act
DynamoDbProvider provider = makeProvider(MultiparamsTestTable);
Map<String, String> values = provider.getMultipleValues("test_param");

// Assert
assertThat(values.size()).isEqualTo(2);
assertThat(values.get("test_param_part_1")).isEqualTo("the_value_is_hello!");
assertThat(values.get("test_param_part_2")).isEqualTo("the_value_is_still_hello!");
}

private DynamoDbProvider makeProvider(String tableName) {
return new DynamoDbProvider(new CacheManager(), DynamoDbClient.builder()
.httpClientBuilder(UrlConnectionHttpClient.builder()).build(),
tableName);
}

}
Loading