From e9ac40f1cf32459d448f925d8ea394c0aadd0eb4 Mon Sep 17 00:00:00 2001 From: Jerome Van Der Linden Date: Fri, 8 Apr 2022 18:27:37 +0200 Subject: [PATCH 1/5] feat: add AppConfig as a parameter provider --- powertools-parameters/pom.xml | 44 +++- .../parameters/AppConfigProvider.java | 218 ++++++++++++++++++ .../powertools/parameters/ParamManager.java | 25 +- .../parameters/AppConfigProviderTest.java | 95 ++++++++ .../ParamManagerIntegrationTest.java | 30 +++ 5 files changed, 410 insertions(+), 2 deletions(-) create mode 100644 powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/AppConfigProvider.java create mode 100644 powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/AppConfigProviderTest.java diff --git a/powertools-parameters/pom.xml b/powertools-parameters/pom.xml index b255d8b13..4c21d7605 100644 --- a/powertools-parameters/pom.xml +++ b/powertools-parameters/pom.xml @@ -69,6 +69,20 @@ + + software.amazon.awssdk + appconfigdata + + + software.amazon.awssdk + apache-client + + + software.amazon.awssdk + netty-nio-client + + + software.amazon.awssdk url-connection-client @@ -118,6 +132,34 @@ aspectjweaver test + + org.junit-pioneer + junit-pioneer + 1.6.2 + test + - + + + jdk16 + + [16,) + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + + + + + + + diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/AppConfigProvider.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/AppConfigProvider.java new file mode 100644 index 000000000..9745f7284 --- /dev/null +++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/AppConfigProvider.java @@ -0,0 +1,218 @@ +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.appconfigdata.AppConfigDataClient; +import software.amazon.awssdk.services.appconfigdata.model.GetLatestConfigurationRequest; +import software.amazon.awssdk.services.appconfigdata.model.GetLatestConfigurationResponse; +import software.amazon.awssdk.services.appconfigdata.model.StartConfigurationSessionRequest; +import software.amazon.awssdk.services.appconfigdata.model.StartConfigurationSessionResponse; +import software.amazon.awssdk.utils.IoUtils; +import software.amazon.lambda.powertools.parameters.cache.CacheManager; +import software.amazon.lambda.powertools.parameters.transform.TransformationManager; +import software.amazon.lambda.powertools.parameters.transform.Transformer; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.time.temporal.ChronoUnit; +import java.util.Map; + +public class AppConfigProvider extends BaseProvider { + + private AppConfigDataClient client; + + AppConfigProvider() { + this(new CacheManager()); + } + + AppConfigProvider(CacheManager cacheManager) { + this(cacheManager, defaultClient()); + } + + AppConfigProvider(CacheManager cacheManager, AppConfigDataClient client) { + super(cacheManager); + this.client = client; + } + + /** + * Create a builder that can be used to configure and create a {@link AppConfigProvider}. + * + * @return a new instance of {@link Builder} + */ + public static Builder builder() { + return new Builder(); + } + + @Override + protected String getValue(String key) { + String[] profile = key.split("/"); + if (profile.length < 3) { + throw new IllegalArgumentException("Your key is incorrect, please specify an 'application', an 'environment' and the 'configuration' separated with '/', eg. '/myapp/prod/myvar'"); + } + int index = key.startsWith("/") ? 1 : 0; + String application = profile[index]; + String environment = profile[index + 1]; + String configuration = profile[index + 2]; + + if (useAppConfigExtension()) { + return getValueWithExtension(application, environment, configuration); + } else { + return getValueWithClient(application, environment, configuration); + } + } + + private String getValueWithClient(String application, String environment, String configuration) { + StartConfigurationSessionResponse sessionResponse = client.startConfigurationSession(StartConfigurationSessionRequest.builder() + .applicationIdentifier(application) + .environmentIdentifier(environment) + .configurationProfileIdentifier(configuration) + .build()); + GetLatestConfigurationResponse configurationResponse = client.getLatestConfiguration(GetLatestConfigurationRequest.builder() + .configurationToken(sessionResponse.initialConfigurationToken()) + .build()); + return configurationResponse.configuration().asUtf8String(); + } + + private String getValueWithExtension(String application, String environment, String configuration) { + try { + HttpURLConnection connection = connectToExtension(application, environment, configuration); + if (connection.getResponseCode() == 200) { + InputStream responseStream = connection.getInputStream(); + return IoUtils.toUtf8String(responseStream); + } + throw new IOException("Error " + connection.getResponseCode() + ": " + connection.getResponseMessage()); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Your key is incorrect, please specify an 'application', an 'environment' and the 'configuration' separated with '/', eg. '/myapp/prod/myvar'", e); + } catch (IOException e) { + throw new IllegalStateException("Cannot connect to the AppConfig extension, please add the extension layer to your function (see https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-integration-lambda-extensions-versions.html)", e); + } + } + + HttpURLConnection connectToExtension(String application, String environment, String configuration) throws IOException { + URL url = new URL(getExtensionUrl(application, environment, configuration)); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Accept", "*/*"); + return connection; + } + + String getExtensionUrl(String application, String environment, String configuration) { + return String.format("http://localhost:2772/applications/%s/environments/%s/configurations/%s", + application, environment, configuration); + } + + /** + * @throws UnsupportedOperationException as it is not possible to get multiple values simultaneously from App Config + */ + @Override + protected Map getMultipleValues(String path) { + throw new UnsupportedOperationException("Impossible to get multiple values from AWS Secrets Manager"); + } + + /** + * {@inheritDoc} + */ + @Override + public AppConfigProvider defaultMaxAge(int maxAge, ChronoUnit unit) { + super.defaultMaxAge(maxAge, unit); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public AppConfigProvider withMaxAge(int maxAge, ChronoUnit unit) { + super.withMaxAge(maxAge, unit); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public AppConfigProvider withTransformation(Class transformerClass) { + super.withTransformation(transformerClass); + return this; + } + + public static boolean useAppConfigExtension() { + String appConfigExtensionEnv = System.getenv().get("POWERTOOLS_APPCONFIG_EXTENSION"); + return appConfigExtensionEnv != null && !appConfigExtensionEnv.equalsIgnoreCase("false"); + } + + private static AppConfigDataClient defaultClient() { + return useAppConfigExtension() ? null : AppConfigDataClient.builder() + .httpClientBuilder(UrlConnectionHttpClient.builder()) + .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) + .region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable()))) + .build(); + } + + static class Builder { + + private AppConfigDataClient client; + private CacheManager cacheManager; + private TransformationManager transformationManager; + + /** + * Create a {@link AppConfigProvider} instance. + * + * @return a {@link AppConfigProvider} + */ + public AppConfigProvider build() { + if (cacheManager == null) { + throw new IllegalStateException("No CacheManager provided, please provide one"); + } + AppConfigProvider provider; + if (client != null) { + provider = new AppConfigProvider(cacheManager, client); + } else { + provider = new AppConfigProvider(cacheManager); + } + if (transformationManager != null) { + provider.setTransformationManager(transformationManager); + } + return provider; + } + + /** + * Set custom {@link AppConfigDataClient} to pass to the {@link AppConfigProvider}.
+ * 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.
builder.withClient().build()
) + */ + public Builder withClient(AppConfigDataClient client) { + this.client = client; + return this; + } + + /** + * Mandatory. Provide a CacheManager to the {@link AppConfigProvider} + * + * @param cacheManager the manager that will handle the cache of parameters + * @return the builder to chain calls (eg.
builder.withCacheManager().build()
) + */ + public Builder withCacheManager(CacheManager cacheManager) { + this.cacheManager = cacheManager; + return this; + } + + /** + * Provide a transformationManager to the {@link AppConfigProvider} + * + * @param transformationManager the manager that will handle transformation of parameters + * @return the builder to chain calls (eg.
builder.withTransformationManager().build()
) + */ + public Builder withTransformationManager(TransformationManager transformationManager) { + this.transformationManager = transformationManager; + return this; + } + } +} diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/ParamManager.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/ParamManager.java index 0131ae179..86bf2f045 100644 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/ParamManager.java +++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/ParamManager.java @@ -13,6 +13,7 @@ */ package software.amazon.lambda.powertools.parameters; +import software.amazon.awssdk.services.appconfigdata.AppConfigDataClient; import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; import software.amazon.awssdk.services.ssm.SsmClient; import software.amazon.lambda.powertools.parameters.cache.CacheManager; @@ -36,7 +37,7 @@ public final class ParamManager { /** * Get a concrete implementation of {@link BaseProvider}.
- * You can specify {@link SecretsProvider} or {@link SSMProvider} or create your custom provider + * You can specify {@link SecretsProvider} or {@link SSMProvider} or {@link AppConfigProvider} or create your custom provider * by extending {@link BaseProvider} if you need to integrate with a different parameter store. * @return a {@link SecretsProvider} */ @@ -65,6 +66,15 @@ public static SSMProvider getSsmProvider() { return getProvider(SSMProvider.class); } + /** + * Get a {@link AppConfigProvider} with default {@link AppConfigDataClient}.
+ * If you need to customize the region, or other part of the client, use {@link ParamManager#getAppConfigProvider(AppConfigDataClient)} instead. + * @return a {@link AppConfigProvider} + */ + public static AppConfigProvider getAppConfigProvider() { + return getProvider(AppConfigProvider.class); + } + /** * Get a {@link SecretsProvider} with your custom {@link SecretsManagerClient}.
* Use this to configure region or other part of the client. Use {@link ParamManager#getSsmProvider()} if you don't need this customization. @@ -91,6 +101,19 @@ public static SSMProvider getSsmProvider(SsmClient client) { .build()); } + /** + * Get a {@link AppConfigProvider} with your custom {@link AppConfigDataClient}.
+ * Use this to configure region or other part of the client. Use {@link ParamManager#getAppConfigProvider()} if you don't need this customization. + * @return a {@link AppConfigProvider} + */ + public static AppConfigProvider getAppConfigProvider(AppConfigDataClient client) { + return (AppConfigProvider) providers.computeIfAbsent(AppConfigProvider.class, (k) -> AppConfigProvider.builder() + .withClient(client) + .withCacheManager(cacheManager) + .withTransformationManager(transformationManager) + .build()); + } + public static CacheManager getCacheManager() { return cacheManager; } diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/AppConfigProviderTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/AppConfigProviderTest.java new file mode 100644 index 000000000..f38391482 --- /dev/null +++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/AppConfigProviderTest.java @@ -0,0 +1,95 @@ +package software.amazon.lambda.powertools.parameters; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.SetEnvironmentVariable; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.Mockito; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.appconfigdata.AppConfigDataClient; +import software.amazon.awssdk.services.appconfigdata.model.GetLatestConfigurationRequest; +import software.amazon.awssdk.services.appconfigdata.model.GetLatestConfigurationResponse; +import software.amazon.awssdk.services.appconfigdata.model.StartConfigurationSessionRequest; +import software.amazon.awssdk.services.appconfigdata.model.StartConfigurationSessionResponse; +import software.amazon.lambda.powertools.parameters.cache.CacheManager; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.MockitoAnnotations.openMocks; + +public class AppConfigProviderTest { + + @Mock + AppConfigDataClient client; + + @Captor + ArgumentCaptor paramCaptor; + @Captor + ArgumentCaptor sessionCaptor; + + CacheManager cacheManager; + + AppConfigProvider provider; + + @BeforeEach + public void init() { + openMocks(this); + cacheManager = new CacheManager(); + provider = new AppConfigProvider(cacheManager, client); + } + + @Test + @SetEnvironmentVariable(key = "POWERTOOLS_APPCONFIG_EXTENSION", value = "true") + public void getValue_withExtension() throws IOException { + AppConfigProvider mockedProvider = Mockito.spy(AppConfigProvider.class); + + HttpURLConnection connection = Mockito.mock(HttpURLConnection.class); + Mockito.when(connection.getResponseCode()).thenReturn(200); + Mockito.when(connection.getInputStream()).thenReturn(new ByteArrayInputStream("{\"key\":\"value\"}".getBytes(StandardCharsets.UTF_8))); + Mockito.when(mockedProvider.connectToExtension("app", "env", "key")).thenReturn(connection); + + String result = mockedProvider.getValue("/app/env/key"); + assertThat(result).isEqualTo("{\"key\":\"value\"}"); + } + + @Test + public void getValue_withClient() { + String key = "/app/env/Key1"; + String expectedValue = "Value1"; + + StartConfigurationSessionResponse session = StartConfigurationSessionResponse.builder().initialConfigurationToken("fakeToken").build(); + Mockito.when(client.startConfigurationSession(sessionCaptor.capture())).thenReturn(session); + + GetLatestConfigurationResponse response = GetLatestConfigurationResponse.builder().configuration(SdkBytes.fromString(expectedValue, StandardCharsets.UTF_8)).build(); + Mockito.when(client.getLatestConfiguration(paramCaptor.capture())).thenReturn(response); + + String value = provider.getValue(key); + + assertThat(value).isEqualTo(expectedValue); + assertThat(paramCaptor.getValue().configurationToken()).isEqualTo("fakeToken"); + assertThat(sessionCaptor.getValue().applicationIdentifier()).isEqualTo("app"); + assertThat(sessionCaptor.getValue().environmentIdentifier()).isEqualTo("env"); + assertThat(sessionCaptor.getValue().configurationProfileIdentifier()).isEqualTo("Key1"); + } + + @Test + public void invalidKey() { + String key = "keyWithoutAppEnvConfig"; + assertThatThrownBy(() -> {provider.getValue(key); }) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Your key is incorrect, please specify an 'application', an 'environment' and the 'configuration' separated with '/', eg. '/myapp/prod/myvar'"); + } + + @Test + public void testExtensionUrl() { + String extensionUrl = provider.getExtensionUrl("myApp", "prod", "config"); + assertThat(extensionUrl).isEqualTo("http://localhost:2772/applications/myApp/environments/prod/configurations/config"); + } +} diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/ParamManagerIntegrationTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/ParamManagerIntegrationTest.java index 0b4a2093f..9edf0484b 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/ParamManagerIntegrationTest.java +++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/ParamManagerIntegrationTest.java @@ -19,12 +19,20 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; +import org.mockito.Mockito; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.appconfigdata.AppConfigDataClient; +import software.amazon.awssdk.services.appconfigdata.model.GetLatestConfigurationRequest; +import software.amazon.awssdk.services.appconfigdata.model.GetLatestConfigurationResponse; +import software.amazon.awssdk.services.appconfigdata.model.StartConfigurationSessionRequest; +import software.amazon.awssdk.services.appconfigdata.model.StartConfigurationSessionResponse; import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest; import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse; import software.amazon.awssdk.services.ssm.SsmClient; import software.amazon.awssdk.services.ssm.model.*; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -53,6 +61,10 @@ public class ParamManagerIntegrationTest { @Captor ArgumentCaptor secretsCaptor; + @Mock + AppConfigDataClient appConfigDataClient; + + @BeforeEach public void setup() throws IllegalAccessException { @@ -116,4 +128,22 @@ public void secretsProvider_get() { assertThat(secretsProvider.get("keys")).isEqualTo(expectedValue); // second time is from cache verify(secretsManagerClient, times(1)).getSecretValue(any(GetSecretValueRequest.class)); } + + @Test + public void appConfigProvider_get() { + AppConfigProvider appConfigProvider = ParamManager.getAppConfigProvider(appConfigDataClient); + + StartConfigurationSessionResponse session = StartConfigurationSessionResponse.builder().initialConfigurationToken("fakeToken").build(); + Mockito.when(appConfigDataClient.startConfigurationSession(any(StartConfigurationSessionRequest.class))).thenReturn(session); + + String expectedValue = "Value1"; + GetLatestConfigurationResponse response = GetLatestConfigurationResponse.builder() + .configuration(SdkBytes.fromString(expectedValue, StandardCharsets.UTF_8)) + .build(); + when(appConfigDataClient.getLatestConfiguration(any(GetLatestConfigurationRequest.class))).thenReturn(response); + + assertThat(appConfigProvider.get("/app/env/key")).isEqualTo(expectedValue); + assertThat(appConfigProvider.get("/app/env/key")).isEqualTo(expectedValue); // second time is from cache + verify(appConfigDataClient, times(1)).getLatestConfiguration(any(GetLatestConfigurationRequest.class)); + } } From dd7bcdba99d920f3ddac845399cddc36d4ce2e95 Mon Sep 17 00:00:00 2001 From: Jerome Van Der Linden Date: Sat, 9 Apr 2022 00:23:17 +0200 Subject: [PATCH 2/5] update doc for parameters --- docs/utilities/parameters.md | 109 +++++++++++++++++++++++++++++++---- 1 file changed, 97 insertions(+), 12 deletions(-) diff --git a/docs/utilities/parameters.md b/docs/utilities/parameters.md index e2d0cb965..dec54a43f 100644 --- a/docs/utilities/parameters.md +++ b/docs/utilities/parameters.md @@ -4,9 +4,13 @@ description: Utility --- -The parameters utility provides a way to retrieve parameter values from -[AWS Systems Manager Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html) or -[AWS Secrets Manager](https://aws.amazon.com/secrets-manager/). It also provides a base class to create your parameter provider implementation. +The parameters utility provides a way to retrieve parameter values from: + + - [AWS Systems Manager Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html) + - [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) + - [AWS AppConfig](https://aws.amazon.com/systems-manager/features/appconfig/) + +It also provides a base class to create your parameter provider implementation. **Key features** @@ -40,11 +44,12 @@ To install this utility, add the following dependency to your project. This utility requires additional permissions to work as expected. See the table below: -Provider | Function/Method | IAM Permission -------------------------------------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------- -SSM Parameter Store | `SSMProvider.get(String)` `SSMProvider.get(String, Class)` | `ssm:GetParameter` -SSM Parameter Store | `SSMProvider.getMultiple(String)` | `ssm:GetParametersByPath` -Secrets Manager | `SecretsProvider.get(String)` `SecretsProvider.get(String, Class)` | `secretsmanager:GetSecretValue` +| Provider | Function/Method | IAM Permission | +|---------------------|------------------------------------------------------------------------|-----------------------------------------------------------------------------| +| SSM Parameter Store | `SSMProvider.get(String)` `SSMProvider.get(String, Class)` | `ssm:GetParameter` | +| SSM Parameter Store | `SSMProvider.getMultiple(String)` | `ssm:GetParametersByPath` | +| Secrets Manager | `SecretsProvider.get(String)` `SecretsProvider.get(String, Class)` | `secretsmanager:GetSecretValue` | +| AppConfig | `AppConfigProvider.get(String)` `AppConfigProvider.get(String, Class)` | `appconfig:GetLatestConfiguration` & `appconfig:StartConfigurationSession` | ## SSM Parameter Store @@ -99,10 +104,10 @@ in order to get data from other regions or use specific credentials. The AWS Systems Manager Parameter Store provider supports two additional arguments for the `get()` and `getMultiple()` methods: -| Option | Default | Description | -|---------------|---------|-------------| -| **withDecryption()** | `False` | Will automatically decrypt the parameter. | -| **recursive()** | `False` | For `getMultiple()` only, will fetch all parameter values recursively based on a path prefix. | +| Option | Default | Description | +|----------------------|---------|-----------------------------------------------------------------------------------------------| +| **withDecryption()** | `False` | Will automatically decrypt the parameter. | +| **recursive()** | `False` | For `getMultiple()` only, will fetch all parameter values recursively based on a path prefix. | **Example:** @@ -166,6 +171,86 @@ in order to get data from other regions or use specific credentials. } ``` +## AppConfig +To retrieve parameters from AppConfig, you can choose to use the [AppConfig Lambda Extension](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-integration-lambda-extensions.html) +or the client SDK. + +!!! info + See how to create configuration in AppConfig in the [documentation](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-working.html). + +=== "AppConfigProvider" + + ```java hl_lines="5 8-9" + import software.amazon.lambda.powertools.parameters.AppConfigProvider; + import software.amazon.lambda.powertools.parameters.ParamManager; + + public class AppWithConfig implements RequestHandler { + // Get an instance of the AppConfig Provider + AppConfigProvider appConfigProvider = ParamManager.getAppConfigProvider(); + + // Retrieve some configuration + // The key must be in form /application/environment/configuration and match your AppConfig setup + String value = appConfigProvider.get("/app/prod/config"); + + } + ``` + +=== "AppConfigProvider with a custom client" + + ```java hl_lines="5 8 11" + import software.amazon.lambda.powertools.parameters.AppConfigProvider; + import software.amazon.lambda.powertools.parameters.ParamManager; + + public class AppWithConfig implements RequestHandler { + AppConfigDataClient client = AppConfigDataClient.builder().region(Region.EU_CENTRAL_1).build(); + + // Get an instance of the AppConfig Provider + AppConfigProvider appConfigProvider = ParamManager.getAppConfigProvider(client); + + // Retrieve a single secret + String value = appConfigProvider.get("/app/prod/config"); + + } + ``` + +### Using the AppConfig Extension for Lambda +To use the extension, add it as a layer to your function and add the `POWERTOOLS_APPCONFIG_EXTENSION` environment variable set to true. +Note that in this case, you cannot customize the client. + +!!! info "Info: Extension ARN" + Make sure you use the ARN for the target region. See the [list](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-integration-lambda-extensions-versions.html). + +=== "SAM configuration" +```yaml hl_lines="7 10" +ParametersFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: Function + Handler: org.demo.parameters.ParametersFunction::handleRequest + Layers: + - arn:aws:lambda:us-east-1:027255383542:layer:AWS-AppConfig-Extension:68 + Environment: + Variables: + POWERTOOLS_APPCONFIG_EXTENSION: 'true' +``` + +=== "AppConfigProvider" + + ```java hl_lines="5 8-9" + import software.amazon.lambda.powertools.parameters.AppConfigProvider; + import software.amazon.lambda.powertools.parameters.ParamManager; + + public class AppWithConfig implements RequestHandler { + // Get an instance of the AppConfig Provider + AppConfigProvider appConfigProvider = ParamManager.getAppConfigProvider(); + + // Retrieve some configuration + // The key must be in form /application/environment/configuration and match your AppConfig setup + String value = appConfigProvider.get("/app/prod/config"); + + } + ``` + ## Advanced configuration ### Caching From 4be3e61150f4d887ffa347e96f53765fc168a6f4 Mon Sep 17 00:00:00 2001 From: Jerome Van Der Linden Date: Sat, 9 Apr 2022 00:26:55 +0200 Subject: [PATCH 3/5] update doc for parameters --- docs/utilities/parameters.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/utilities/parameters.md b/docs/utilities/parameters.md index dec54a43f..70d3da78d 100644 --- a/docs/utilities/parameters.md +++ b/docs/utilities/parameters.md @@ -180,7 +180,7 @@ or the client SDK. === "AppConfigProvider" - ```java hl_lines="5 8-9" + ```java hl_lines="5 9-10" import software.amazon.lambda.powertools.parameters.AppConfigProvider; import software.amazon.lambda.powertools.parameters.ParamManager; @@ -236,7 +236,7 @@ ParametersFunction: === "AppConfigProvider" - ```java hl_lines="5 8-9" + ```java hl_lines="5 9-10" import software.amazon.lambda.powertools.parameters.AppConfigProvider; import software.amazon.lambda.powertools.parameters.ParamManager; From 335be601b2274894f38fcc85bc49e4ac704666f1 Mon Sep 17 00:00:00 2001 From: Jerome Van Der Linden Date: Thu, 14 Apr 2022 16:27:47 +0200 Subject: [PATCH 4/5] remove extension as per #827 --- .../parameters/AppConfigProvider.java | 51 +----------------- .../parameters/AppConfigProviderTest.java | 52 ++++++++++--------- 2 files changed, 29 insertions(+), 74 deletions(-) diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/AppConfigProvider.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/AppConfigProvider.java index 9745f7284..d2e743812 100644 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/AppConfigProvider.java +++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/AppConfigProvider.java @@ -9,16 +9,10 @@ import software.amazon.awssdk.services.appconfigdata.model.GetLatestConfigurationResponse; import software.amazon.awssdk.services.appconfigdata.model.StartConfigurationSessionRequest; import software.amazon.awssdk.services.appconfigdata.model.StartConfigurationSessionResponse; -import software.amazon.awssdk.utils.IoUtils; import software.amazon.lambda.powertools.parameters.cache.CacheManager; import software.amazon.lambda.powertools.parameters.transform.TransformationManager; import software.amazon.lambda.powertools.parameters.transform.Transformer; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; import java.time.temporal.ChronoUnit; import java.util.Map; @@ -26,10 +20,6 @@ public class AppConfigProvider extends BaseProvider { private AppConfigDataClient client; - AppConfigProvider() { - this(new CacheManager()); - } - AppConfigProvider(CacheManager cacheManager) { this(cacheManager, defaultClient()); } @@ -59,11 +49,7 @@ protected String getValue(String key) { String environment = profile[index + 1]; String configuration = profile[index + 2]; - if (useAppConfigExtension()) { - return getValueWithExtension(application, environment, configuration); - } else { - return getValueWithClient(application, environment, configuration); - } + return getValueWithClient(application, environment, configuration); } private String getValueWithClient(String application, String environment, String configuration) { @@ -78,34 +64,6 @@ private String getValueWithClient(String application, String environment, String return configurationResponse.configuration().asUtf8String(); } - private String getValueWithExtension(String application, String environment, String configuration) { - try { - HttpURLConnection connection = connectToExtension(application, environment, configuration); - if (connection.getResponseCode() == 200) { - InputStream responseStream = connection.getInputStream(); - return IoUtils.toUtf8String(responseStream); - } - throw new IOException("Error " + connection.getResponseCode() + ": " + connection.getResponseMessage()); - } catch (MalformedURLException e) { - throw new IllegalArgumentException("Your key is incorrect, please specify an 'application', an 'environment' and the 'configuration' separated with '/', eg. '/myapp/prod/myvar'", e); - } catch (IOException e) { - throw new IllegalStateException("Cannot connect to the AppConfig extension, please add the extension layer to your function (see https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-integration-lambda-extensions-versions.html)", e); - } - } - - HttpURLConnection connectToExtension(String application, String environment, String configuration) throws IOException { - URL url = new URL(getExtensionUrl(application, environment, configuration)); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("GET"); - connection.setRequestProperty("Accept", "*/*"); - return connection; - } - - String getExtensionUrl(String application, String environment, String configuration) { - return String.format("http://localhost:2772/applications/%s/environments/%s/configurations/%s", - application, environment, configuration); - } - /** * @throws UnsupportedOperationException as it is not possible to get multiple values simultaneously from App Config */ @@ -141,13 +99,8 @@ public AppConfigProvider withTransformation(Class transfo return this; } - public static boolean useAppConfigExtension() { - String appConfigExtensionEnv = System.getenv().get("POWERTOOLS_APPCONFIG_EXTENSION"); - return appConfigExtensionEnv != null && !appConfigExtensionEnv.equalsIgnoreCase("false"); - } - private static AppConfigDataClient defaultClient() { - return useAppConfigExtension() ? null : AppConfigDataClient.builder() + return AppConfigDataClient.builder() .httpClientBuilder(UrlConnectionHttpClient.builder()) .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) .region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable()))) diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/AppConfigProviderTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/AppConfigProviderTest.java index f38391482..e6d04ad3e 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/AppConfigProviderTest.java +++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/AppConfigProviderTest.java @@ -2,7 +2,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junitpioneer.jupiter.SetEnvironmentVariable; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; @@ -14,14 +13,15 @@ import software.amazon.awssdk.services.appconfigdata.model.StartConfigurationSessionRequest; import software.amazon.awssdk.services.appconfigdata.model.StartConfigurationSessionResponse; import software.amazon.lambda.powertools.parameters.cache.CacheManager; +import software.amazon.lambda.powertools.parameters.transform.TransformationManager; +import software.amazon.lambda.powertools.parameters.transform.Transformer; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.net.HttpURLConnection; import java.nio.charset.StandardCharsets; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.data.MapEntry.entry; import static org.mockito.MockitoAnnotations.openMocks; public class AppConfigProviderTest { @@ -42,21 +42,7 @@ public class AppConfigProviderTest { public void init() { openMocks(this); cacheManager = new CacheManager(); - provider = new AppConfigProvider(cacheManager, client); - } - - @Test - @SetEnvironmentVariable(key = "POWERTOOLS_APPCONFIG_EXTENSION", value = "true") - public void getValue_withExtension() throws IOException { - AppConfigProvider mockedProvider = Mockito.spy(AppConfigProvider.class); - - HttpURLConnection connection = Mockito.mock(HttpURLConnection.class); - Mockito.when(connection.getResponseCode()).thenReturn(200); - Mockito.when(connection.getInputStream()).thenReturn(new ByteArrayInputStream("{\"key\":\"value\"}".getBytes(StandardCharsets.UTF_8))); - Mockito.when(mockedProvider.connectToExtension("app", "env", "key")).thenReturn(connection); - - String result = mockedProvider.getValue("/app/env/key"); - assertThat(result).isEqualTo("{\"key\":\"value\"}"); + provider = AppConfigProvider.builder().withCacheManager(cacheManager).withTransformationManager(new TransformationManager()).withClient(client).build(); } @Test @@ -79,6 +65,28 @@ public void getValue_withClient() { assertThat(sessionCaptor.getValue().configurationProfileIdentifier()).isEqualTo("Key1"); } + @Test + public void getWithTransformer() { + String key = "/app/env/Key2"; + String expectedValue = "{\"foo\":\"Foo\", \"bar\":42, \"baz\":123456789}"; + + StartConfigurationSessionResponse session = StartConfigurationSessionResponse.builder().initialConfigurationToken("sessionToken").build(); + Mockito.when(client.startConfigurationSession(sessionCaptor.capture())).thenReturn(session); + + GetLatestConfigurationResponse response = GetLatestConfigurationResponse.builder().configuration(SdkBytes.fromString(expectedValue, StandardCharsets.UTF_8)).build(); + Mockito.when(client.getLatestConfiguration(paramCaptor.capture())).thenReturn(response); + + Map map = provider.withTransformation(Transformer.json).get(key, Map.class); + assertThat(map).contains( + entry("foo", "Foo"), + entry("bar", 42), + entry("baz", 123456789)); + assertThat(paramCaptor.getValue().configurationToken()).isEqualTo("sessionToken"); + assertThat(sessionCaptor.getValue().applicationIdentifier()).isEqualTo("app"); + assertThat(sessionCaptor.getValue().environmentIdentifier()).isEqualTo("env"); + assertThat(sessionCaptor.getValue().configurationProfileIdentifier()).isEqualTo("Key2"); + } + @Test public void invalidKey() { String key = "keyWithoutAppEnvConfig"; @@ -86,10 +94,4 @@ public void invalidKey() { .isInstanceOf(IllegalArgumentException.class) .hasMessage("Your key is incorrect, please specify an 'application', an 'environment' and the 'configuration' separated with '/', eg. '/myapp/prod/myvar'"); } - - @Test - public void testExtensionUrl() { - String extensionUrl = provider.getExtensionUrl("myApp", "prod", "config"); - assertThat(extensionUrl).isEqualTo("http://localhost:2772/applications/myApp/environments/prod/configurations/config"); - } } From f7000c98f9c27fdfbfd940e6a873e27c43a44c5c Mon Sep 17 00:00:00 2001 From: Jerome Van Der Linden Date: Thu, 14 Apr 2022 16:33:53 +0200 Subject: [PATCH 5/5] remove extension in the doc as per #827 --- docs/utilities/parameters.md | 40 ------------------------------------ 1 file changed, 40 deletions(-) diff --git a/docs/utilities/parameters.md b/docs/utilities/parameters.md index 70d3da78d..c546d96a2 100644 --- a/docs/utilities/parameters.md +++ b/docs/utilities/parameters.md @@ -172,8 +172,6 @@ in order to get data from other regions or use specific credentials. ``` ## AppConfig -To retrieve parameters from AppConfig, you can choose to use the [AppConfig Lambda Extension](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-integration-lambda-extensions.html) -or the client SDK. !!! info See how to create configuration in AppConfig in the [documentation](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-working.html). @@ -213,44 +211,6 @@ or the client SDK. } ``` -### Using the AppConfig Extension for Lambda -To use the extension, add it as a layer to your function and add the `POWERTOOLS_APPCONFIG_EXTENSION` environment variable set to true. -Note that in this case, you cannot customize the client. - -!!! info "Info: Extension ARN" - Make sure you use the ARN for the target region. See the [list](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-integration-lambda-extensions-versions.html). - -=== "SAM configuration" -```yaml hl_lines="7 10" -ParametersFunction: - Type: AWS::Serverless::Function - Properties: - CodeUri: Function - Handler: org.demo.parameters.ParametersFunction::handleRequest - Layers: - - arn:aws:lambda:us-east-1:027255383542:layer:AWS-AppConfig-Extension:68 - Environment: - Variables: - POWERTOOLS_APPCONFIG_EXTENSION: 'true' -``` - -=== "AppConfigProvider" - - ```java hl_lines="5 9-10" - import software.amazon.lambda.powertools.parameters.AppConfigProvider; - import software.amazon.lambda.powertools.parameters.ParamManager; - - public class AppWithConfig implements RequestHandler { - // Get an instance of the AppConfig Provider - AppConfigProvider appConfigProvider = ParamManager.getAppConfigProvider(); - - // Retrieve some configuration - // The key must be in form /application/environment/configuration and match your AppConfig setup - String value = appConfigProvider.get("/app/prod/config"); - - } - ``` - ## Advanced configuration ### Caching