Skip to content

Commit

Permalink
chore: Add powertools specific user-agent-suffix to the AWS SDK v2 cl…
Browse files Browse the repository at this point in the history
…ients (#1306)
  • Loading branch information
eldimi authored Aug 2, 2023
1 parent 6900b72 commit 7179713
Show file tree
Hide file tree
Showing 13 changed files with 304 additions and 12 deletions.
11 changes: 11 additions & 0 deletions powertools-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>${log4j.version}</version>
</dependency>

<!-- Test dependencies -->
<dependency>
Expand Down Expand Up @@ -104,6 +109,12 @@
</dependencies>

<build>
<resources>
<resource>
<directory>src/main/resources-filtered</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package software.amazon.lambda.powertools.core.internal;

import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getenv;

import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.util.Properties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
* Can be used to create a string that can server as a User-Agent suffix in requests made with the AWS SDK clients
*/
public class UserAgentConfigurator {

public static final String NA = "NA";
public static final String VERSION_KEY = "powertools.version";
public static final String PT_FEATURE_VARIABLE = "${PT_FEATURE}";
public static final String PT_EXEC_ENV_VARIABLE = "${PT_EXEC_ENV}";
public static final String VERSION_PROPERTIES_FILENAME = "version.properties";
public static final String AWS_EXECUTION_ENV = "AWS_EXECUTION_ENV";
private static final Logger LOG = LoggerFactory.getLogger(UserAgentConfigurator.class);
private static final String NO_OP = "no-op";
private static String ptVersion = getProjectVersion();
private static String userAgentPattern = "PT/" + PT_FEATURE_VARIABLE + "/" + ptVersion + " PTEnv/"
+ PT_EXEC_ENV_VARIABLE;

private UserAgentConfigurator() {
throw new IllegalStateException("Utility class. Not meant to be instantiated");
}

/**
* Retrieves the project version from the version.properties file
*
* @return the project version
*/
static String getProjectVersion() {
return getVersionFromProperties(VERSION_PROPERTIES_FILENAME, VERSION_KEY);
}


/**
* Retrieves the project version from a properties file.
* The file should be in the resources folder.
* The version is retrieved from the property with the given key.
*
* @param propertyFileName the name of the properties file
* @param versionKey the key of the property that contains the version
* @return the version of the project as configured in the given properties file
*/
static String getVersionFromProperties(String propertyFileName, String versionKey) {

URL propertiesFileURI = Thread.currentThread().getContextClassLoader().getResource(propertyFileName);
if (propertiesFileURI != null) {
try (FileInputStream fis = new FileInputStream(propertiesFileURI.getPath())) {
Properties properties = new Properties();
properties.load(fis);
String version = properties.getProperty(versionKey);
if (version != null && !version.isEmpty()) {
return version;
}
} catch (IOException e) {
LOG.warn("Unable to read {} file. Using default version.", propertyFileName);
LOG.debug("Exception:", e);
}
}
return NA;
}

/**
* Retrieves the user agent string for the Powertools for AWS Lambda.
* It follows the pattern PT/{PT_FEATURE}/{PT_VERSION} PTEnv/{PT_EXEC_ENV}
* The version of the project is automatically retrieved.
* The PT_EXEC_ENV is automatically retrieved from the AWS_EXECUTION_ENV environment variable.
* If it AWS_EXECUTION_ENV is not set, PT_EXEC_ENV defaults to "NA"
*
* @param ptFeature a custom feature to be added to the user agent string (e.g. idempotency).
* If null or empty, the default PT_FEATURE is used.
* The default PT_FEATURE is "no-op".
* @return the user agent string
*/
public static String getUserAgent(String ptFeature) {

String awsExecutionEnv = getenv(AWS_EXECUTION_ENV);
String ptExecEnv = awsExecutionEnv != null ? awsExecutionEnv : NA;
String userAgent = userAgentPattern.replace(PT_EXEC_ENV_VARIABLE, ptExecEnv);

if (ptFeature == null || ptFeature.isEmpty()) {
ptFeature = NO_OP;
}
return userAgent
.replace(PT_FEATURE_VARIABLE, ptFeature)
.replace(PT_EXEC_ENV_VARIABLE, ptExecEnv);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# The filtered properties can have variables that are filled in by system properties or project properties.
# See https://maven.apache.org/plugins/maven-resources-plugin/examples/filter.html
#
# The values are replaced before copying the resources to the main output directory. Therefore, as soon as the build phase is completed,
# the values should have been replaced if the properties are available and if 'filtering' is set to true in the pom.xml
#
#
# The ${project.version} is retrieved from the respective pom.xml property
powertools.version=${project.version}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package software.amazon.lambda.powertools.core.internal;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mockStatic;
import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getenv;
import static software.amazon.lambda.powertools.core.internal.UserAgentConfigurator.AWS_EXECUTION_ENV;
import static software.amazon.lambda.powertools.core.internal.UserAgentConfigurator.VERSION_KEY;
import static software.amazon.lambda.powertools.core.internal.UserAgentConfigurator.VERSION_PROPERTIES_FILENAME;
import static software.amazon.lambda.powertools.core.internal.UserAgentConfigurator.getVersionFromProperties;

import java.io.File;
import java.util.Objects;
import java.util.regex.Pattern;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;

class UserAgentConfiguratorTest {

private static final String SEM_VER_PATTERN = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$";
private static final String VERSION = UserAgentConfigurator.getProjectVersion();


@Test
void testGetVersion() {

assertThat(VERSION)
.isNotNull()
.isNotEmpty();
assertThat(Pattern.matches(SEM_VER_PATTERN, VERSION)).isTrue();
}

@Test
void testGetVersionFromProperties_WrongKey() {
String version = getVersionFromProperties(VERSION_PROPERTIES_FILENAME, "some invalid key");

assertThat(version)
.isNotNull()
.isEqualTo("NA");
}

@Test
void testGetVersionFromProperties_FileNotExist() {
String version = getVersionFromProperties("some file", VERSION_KEY);

assertThat(version)
.isNotNull()
.isEqualTo("NA");
}

@Test
void testGetVersionFromProperties_InvalidFile() {
File f = new File(Objects.requireNonNull(Thread.currentThread().getContextClassLoader()
.getResource("unreadable.properties")).getPath());
f.setReadable(false);

String version = getVersionFromProperties("unreadable.properties", VERSION_KEY);

assertThat(version).isEqualTo("NA");
}

@Test
void testGetVersionFromProperties_EmptyVersion() {
String version = getVersionFromProperties("test.properties", VERSION_KEY);

assertThat(version).isEqualTo("NA");
}

@Test
void testGetUserAgent() {
String userAgent = UserAgentConfigurator.getUserAgent("test-feature");

assertThat(userAgent)
.isNotNull()
.isEqualTo("PT/test-feature/" + VERSION + " PTEnv/NA");

}

@Test
void testGetUserAgent_NoFeature() {
String userAgent = UserAgentConfigurator.getUserAgent("");

assertThat(userAgent)
.isNotNull()
.isEqualTo("PT/no-op/" + VERSION + " PTEnv/NA");
}

@Test
void testGetUserAgent_NullFeature() {
String userAgent = UserAgentConfigurator.getUserAgent(null);

assertThat(userAgent)
.isNotNull()
.isEqualTo("PT/no-op/" + VERSION + " PTEnv/NA");
}

@Test
void testGetUserAgent_SetAWSExecutionEnv() {
try (MockedStatic<SystemWrapper> mockedSystemWrapper = mockStatic(SystemWrapper.class)) {
mockedSystemWrapper.when(() -> getenv(AWS_EXECUTION_ENV)).thenReturn("AWS_Lambda_java8");
String userAgent = UserAgentConfigurator.getUserAgent("test-feature");

assertThat(userAgent)
.isNotNull()
.isEqualTo("PT/test-feature/" + VERSION + " PTEnv/AWS_Lambda_java8");
}
}

}
1 change: 1 addition & 0 deletions powertools-core/src/test/resources/test.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
powertools.version=
2 changes: 2 additions & 0 deletions powertools-core/src/test/resources/unreadable.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# This is intentionally left empty
# It used during testing and is set to un-readable to fulfil the test purposes.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@

package software.amazon.lambda.powertools.idempotency.persistence;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption;
import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.utils.StringUtils;
import software.amazon.lambda.powertools.core.internal.UserAgentConfigurator;
import software.amazon.lambda.powertools.idempotency.Constants;
import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemAlreadyExistsException;
import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemNotFoundException;
import static software.amazon.lambda.powertools.core.internal.LambdaConstants.AWS_REGION_ENV;
import static software.amazon.lambda.powertools.core.internal.LambdaConstants.LAMBDA_FUNCTION_NAME_ENV;
import static software.amazon.lambda.powertools.idempotency.persistence.DataRecord.Status.INPROGRESS;
Expand All @@ -25,22 +37,14 @@
import java.util.OptionalLong;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException;
import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest;
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
import software.amazon.awssdk.utils.StringUtils;
import software.amazon.lambda.powertools.idempotency.Constants;
import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemAlreadyExistsException;
import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemNotFoundException;

/**
* DynamoDB version of the {@link PersistenceStore}. Will store idempotency data in DynamoDB.<br>
Expand All @@ -49,6 +53,7 @@
public class DynamoDBPersistenceStore extends BasePersistenceStore implements PersistenceStore {

private static final Logger LOG = LoggerFactory.getLogger(DynamoDBPersistenceStore.class);
public static final String IDEMPOTENCY = "idempotency";

private final String tableName;
private final String keyAttr;
Expand Down Expand Up @@ -92,6 +97,7 @@ private DynamoDBPersistenceStore(String tableName,
if (idempotencyDisabledEnv == null || "false".equalsIgnoreCase(idempotencyDisabledEnv)) {
this.dynamoDbClient = DynamoDbClient.builder()
.httpClient(UrlConnectionHttpClient.builder().build())
.overrideConfiguration(ClientOverrideConfiguration.builder().putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, UserAgentConfigurator.getUserAgent(IDEMPOTENCY)).build())
.region(Region.of(System.getenv(AWS_REGION_ENV)))
.build();
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@
import java.util.HashMap;
import java.util.Map;
import software.amazon.awssdk.core.SdkSystemSetting;
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption;
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.lambda.powertools.core.internal.UserAgentConfigurator;
import software.amazon.lambda.powertools.parameters.cache.CacheManager;
import software.amazon.lambda.powertools.parameters.transform.TransformationManager;

Expand Down Expand Up @@ -153,6 +156,8 @@ public AppConfigProvider build() {
client = AppConfigDataClient.builder()
.httpClientBuilder(UrlConnectionHttpClient.builder())
.region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable())))
.overrideConfiguration(ClientOverrideConfiguration.builder()
.putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, UserAgentConfigurator.getUserAgent(PARAMETERS)).build())
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
*/
@NotThreadSafe
public abstract class BaseProvider implements ParamProvider {
public static final String PARAMETERS = "parameters";

protected final CacheManager cacheManager;
private TransformationManager transformationManager;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import java.util.Map;
import java.util.stream.Collectors;
import software.amazon.awssdk.core.SdkSystemSetting;
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption;
import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
Expand All @@ -26,6 +28,7 @@
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
import software.amazon.awssdk.services.dynamodb.model.QueryRequest;
import software.amazon.awssdk.services.dynamodb.model.QueryResponse;
import software.amazon.lambda.powertools.core.internal.UserAgentConfigurator;
import software.amazon.lambda.powertools.parameters.cache.CacheManager;
import software.amazon.lambda.powertools.parameters.exception.DynamoDbProviderSchemaException;
import software.amazon.lambda.powertools.parameters.transform.TransformationManager;
Expand Down Expand Up @@ -132,6 +135,7 @@ private static DynamoDbClient createClient() {
return DynamoDbClient.builder()
.httpClientBuilder(UrlConnectionHttpClient.builder())
.region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable())))
.overrideConfiguration(ClientOverrideConfiguration.builder().putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, UserAgentConfigurator.getUserAgent(PARAMETERS)).build())
.build();
}

Expand Down
Loading

0 comments on commit 7179713

Please sign in to comment.