diff --git a/.changes/next-release/feature-AWSSDKforJavav2-0277feb.json b/.changes/next-release/feature-AWSSDKforJavav2-0277feb.json new file mode 100644 index 000000000000..898e23895f2c --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-0277feb.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Adds an option to set 'appId' metadata to the client builder or to system settings and config files. This metadata string value will be added to the user agent string as `app/somevalue`" +} diff --git a/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileProperty.java b/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileProperty.java index 4e7e760eb607..0ec8103ff87d 100644 --- a/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileProperty.java +++ b/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileProperty.java @@ -177,6 +177,12 @@ public final class ProfileProperty { */ public static final String ENDPOINT_URL = "endpoint_url"; + /** + * Configure an optional identification value to be appended to the user agent header. + * The value should be less than 50 characters in length and is null by default. + */ + public static final String SDK_UA_APP_ID = "sdk_ua_app_id"; + private ProfileProperty() { } } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java index e995165fdcc9..87af58b9ad7b 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java @@ -224,7 +224,13 @@ public enum SdkSystemSetting implements SystemSetting { * Defines a file path from which partition metadata should be loaded. If this isn't specified, the partition * metadata deployed with the SDK client will be used instead. */ - AWS_PARTITIONS_FILE("aws.partitionsFile", null) + AWS_PARTITIONS_FILE("aws.partitionsFile", null), + + /** + * Configure an optional identification value to be appended to the user agent header. + * The value should be less than 50 characters in length and is null by default. + */ + AWS_SDK_UA_APP_ID("sdk.ua.appId", null) ; diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkClientBuilder.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkClientBuilder.java index d106d6d27879..b798724bde04 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkClientBuilder.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkClientBuilder.java @@ -95,4 +95,5 @@ default B addPlugin(SdkPlugin plugin) { default List plugins() { throw new UnsupportedOperationException(); } + } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java index 7dd7e99a0c90..6596a7065a7a 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java @@ -48,6 +48,8 @@ import static software.amazon.awssdk.core.client.config.SdkClientOption.RETRY_STRATEGY; import static software.amazon.awssdk.core.client.config.SdkClientOption.SCHEDULED_EXECUTOR_SERVICE; import static software.amazon.awssdk.core.client.config.SdkClientOption.SYNC_HTTP_CLIENT; +import static software.amazon.awssdk.core.client.config.SdkClientOption.USER_AGENT_APP_ID; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.APP_ID; import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.HTTP; import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.INTERNAL_METADATA_MARKER; import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.IO; @@ -90,6 +92,7 @@ import software.amazon.awssdk.core.internal.http.pipeline.stages.CompressRequestStage; import software.amazon.awssdk.core.internal.interceptor.HttpChecksumValidationInterceptor; import software.amazon.awssdk.core.internal.retry.SdkDefaultRetryStrategy; +import software.amazon.awssdk.core.internal.useragent.AppIdResolver; import software.amazon.awssdk.core.internal.useragent.SdkClientUserAgentProperties; import software.amazon.awssdk.core.internal.useragent.SdkUserAgentBuilder; import software.amazon.awssdk.core.retry.RetryMode; @@ -151,7 +154,6 @@ public abstract class SdkDefaultClientBuilder, private final List plugins = new ArrayList<>(); - protected SdkDefaultClientBuilder() { this(DEFAULT_HTTP_CLIENT_BUILDER, DEFAULT_ASYNC_HTTP_CLIENT_BUILDER); } @@ -413,7 +415,7 @@ private String resolveClientUserAgent(LazyValueSource config) { SdkClientUserAgentProperties clientProperties = new SdkClientUserAgentProperties(); ClientType clientType = config.get(CLIENT_TYPE); - ClientType resolvedClientType = clientType == null ? ClientType.UNKNOWN : config.get(CLIENT_TYPE); + ClientType resolvedClientType = clientType == null ? ClientType.UNKNOWN : clientType; clientProperties.putProperty(RETRY_MODE, StringUtils.lowerCase(resolveRetryMode(config.get(RETRY_POLICY), config.get(RETRY_STRATEGY)))); @@ -422,10 +424,20 @@ private String resolveClientUserAgent(LazyValueSource config) { clientProperties.putProperty(HTTP, SdkHttpUtils.urlEncode(clientName(resolvedClientType, config.get(SYNC_HTTP_CLIENT), config.get(ASYNC_HTTP_CLIENT)))); - + String appId = config.get(USER_AGENT_APP_ID); + String resolvedAppId = appId == null ? resolveAppId(config) : appId; + clientProperties.putProperty(APP_ID, resolvedAppId); return SdkUserAgentBuilder.buildClientUserAgentString(SystemUserAgent.getOrCreate(), clientProperties); } + private String resolveAppId(LazyValueSource config) { + Optional appIdFromConfig = AppIdResolver.create() + .profileFile(config.get(PROFILE_FILE_SUPPLIER)) + .profileName(config.get(PROFILE_NAME)) + .resolve(); + return appIdFromConfig.orElse(null); + } + private static String clientName(ClientType clientType, SdkHttpClient syncHttpClient, SdkAsyncHttpClient asyncHttpClient) { if (clientType == SYNC) { return syncHttpClient == null ? "null" : syncHttpClient.clientName(); @@ -446,7 +458,7 @@ private RetryStrategy resolveRetryStrategy(LazyValueSource config) { .resolve(); return SdkDefaultRetryStrategy.forRetryMode(retryMode); } - + /** * Finalize which sync HTTP client will be used for the created client. */ diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/ClientOverrideConfiguration.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/ClientOverrideConfiguration.java index 204a695397fc..04c418c1b08d 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/ClientOverrideConfiguration.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/ClientOverrideConfiguration.java @@ -34,6 +34,7 @@ import static software.amazon.awssdk.core.client.config.SdkClientOption.RETRY_POLICY; import static software.amazon.awssdk.core.client.config.SdkClientOption.RETRY_STRATEGY; import static software.amazon.awssdk.core.client.config.SdkClientOption.SCHEDULED_EXECUTOR_SERVICE; +import static software.amazon.awssdk.core.client.config.SdkClientOption.USER_AGENT_APP_ID; import static software.amazon.awssdk.utils.ScheduledExecutorUtils.unmanagedScheduledExecutor; import static software.amazon.awssdk.utils.ScheduledExecutorUtils.unwrapUnmanagedScheduledExecutor; @@ -120,6 +121,7 @@ public final class ClientOverrideConfiguration options.add(CONFIGURED_RETRY_STRATEGY); options.add(CONFIGURED_RETRY_CONFIGURATOR); options.add(CONFIGURED_RETRY_MODE); + options.add(USER_AGENT_APP_ID); CLIENT_OVERRIDE_OPTIONS = Collections.unmodifiableSet(options); Set> resolvedOptions = new HashSet<>(); @@ -381,6 +383,14 @@ public Optional compressionConfiguration() { return Optional.ofNullable(compressionConfig); } + /** + * An optional user specified identification value to be appended to the user agent header. + * For more information, see {@link SdkClientOption#USER_AGENT_APP_ID}. + */ + public Optional appId() { + return Optional.ofNullable(config.option(USER_AGENT_APP_ID)); + } + @Override public String toString() { return ToString.builder("ClientOverrideConfiguration") @@ -395,6 +405,7 @@ public String toString() { .add("profileName", defaultProfileName().orElse(null)) .add("scheduledExecutorService", scheduledExecutorService().orElse(null)) .add("compressionConfiguration", compressionConfiguration().orElse(null)) + .add("appId", appId().orElse(null)) .build(); } @@ -757,6 +768,16 @@ default Builder compressionConfiguration(Consumer extends ClientOption { public static final SdkClientOption COMPRESSION_CONFIGURATION = new SdkClientOption<>(CompressionConfiguration.class); + /** + * An optional identification value to be appended to the user agent header. + * The value should be less than 50 characters in length and is null by default. + *

+ * Users can additionally supply the appId value through environment and JVM settings, and + * it will be resolved using the following order of precedence (highest first): + *

    + *
  1. This client option configuration
  2. + *
  3. The {@code AWS_SDK_UA_APP_ID} environment variable
  4. + *
  5. The {@code sdk.ua.appId} JVM system property
  6. + *
  7. The {@code sdk_ua_app_id} setting in the profile file for the active profile
  8. + *
+ *

+ * This configuration option supersedes {@link SdkAdvancedClientOption#USER_AGENT_PREFIX} and + * {@link SdkAdvancedClientOption#USER_AGENT_SUFFIX} and should be used instead of those options. + */ + public static final SdkClientOption USER_AGENT_APP_ID = new SdkClientOption<>(String.class); + /** * Option to specify a reference to the SDK client in use. */ diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/AppIdResolver.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/AppIdResolver.java new file mode 100644 index 000000000000..b4577769b57b --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/AppIdResolver.java @@ -0,0 +1,66 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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.awssdk.core.internal.useragent; + +import java.util.Optional; +import java.util.function.Supplier; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.profiles.ProfileFile; +import software.amazon.awssdk.profiles.ProfileFileSystemSetting; +import software.amazon.awssdk.profiles.ProfileProperty; +import software.amazon.awssdk.utils.OptionalUtils; + +@SdkInternalApi +public final class AppIdResolver { + + private Supplier profileFile; + private String profileName; + + private AppIdResolver() { + } + + public static AppIdResolver create() { + return new AppIdResolver(); + } + + public AppIdResolver profileFile(Supplier profileFile) { + this.profileFile = profileFile; + return this; + } + + public AppIdResolver profileName(String profileName) { + this.profileName = profileName; + return this; + } + + public Optional resolve() { + return OptionalUtils.firstPresent(fromSystemSettings(), + () -> fromProfileFile(profileFile, profileName)); + } + + private Optional fromSystemSettings() { + return SdkSystemSetting.AWS_SDK_UA_APP_ID.getStringValue(); + } + + private Optional fromProfileFile(Supplier profileFile, String profileName) { + profileFile = profileFile != null ? profileFile : ProfileFile::defaultProfileFile; + profileName = profileName != null ? profileName : ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow(); + return profileFile.get() + .profile(profileName) + .flatMap(p -> p.property(ProfileProperty.SDK_UA_APP_ID)); + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/SdkUserAgentBuilder.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/SdkUserAgentBuilder.java index 213653cbd307..c2e311c92a2f 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/SdkUserAgentBuilder.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/SdkUserAgentBuilder.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.core.internal.useragent; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.APP_ID; import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.CONFIG_METADATA; import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.ENV_METADATA; import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.HTTP; @@ -34,6 +35,7 @@ import software.amazon.awssdk.annotations.SdkProtectedApi; import software.amazon.awssdk.annotations.ThreadSafe; import software.amazon.awssdk.core.util.SystemUserAgent; +import software.amazon.awssdk.utils.Logger; import software.amazon.awssdk.utils.StringUtils; /** @@ -43,6 +45,8 @@ @SdkProtectedApi public final class SdkUserAgentBuilder { + private static final Logger log = Logger.loggerFor(SdkUserAgentBuilder.class); + private SdkUserAgentBuilder() { } @@ -77,6 +81,12 @@ public static String buildClientUserAgentString(SystemUserAgent systemValues, appendFieldAndSpace(uaString, CONFIG_METADATA, uaPair(RETRY_MODE, retryMode)); } + String appId = userAgentProperties.getProperty(APP_ID); + if (!StringUtils.isEmpty(appId)) { + checkLengthAndWarn(appId); + appendFieldAndSpace(uaString, APP_ID, appId); + } + removeFinalWhitespace(uaString); return uaString.toString(); } @@ -124,4 +134,12 @@ private static void appendAdditionalJvmMetadata(StringBuilder builder, SystemUse appendNonEmptyField(builder, METADATA, lang); } } + + private static void checkLengthAndWarn(String appId) { + if (appId.length() > 50) { + log.warn(() -> String.format("The configured appId '%s' is longer than the recommended maximum length of 50. " + + "This could result in not being able to transmit and log the whole user agent string, " + + "including the complete value of this string.", appId)); + } + } } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/UserAgentConstant.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/UserAgentConstant.java index 179f54df965b..80f235d267f4 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/UserAgentConstant.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/UserAgentConstant.java @@ -36,6 +36,7 @@ public final class UserAgentConstant { public static final String FRAMEWORK_METADATA = "lib"; public static final String METADATA = "md"; public static final String INTERNAL_METADATA_MARKER = "internal"; + public static final String APP_ID = "app"; //Separators used in SDK user agent public static final String SLASH = "/"; diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java index 25e83b36383c..9a9b8c8a8c3d 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java @@ -382,7 +382,7 @@ public void explicitAsyncHttpClientProvided_ClientIsNotManagedBySdk() { public void clientBuilderFieldsHaveBeanEquivalents() throws Exception { // Mutating properties might not have bean equivalents. This is probably fine, since very few customers require // bean-equivalent methods and it's not clear what they'd expect them to be named anyway. Ignore these methods for now. - Set NON_BEAN_EQUIVALENT_METHODS = ImmutableSet.of("addPlugin", "plugins", "putAuthScheme"); + Set NON_BEAN_EQUIVALENT_METHODS = ImmutableSet.of("addPlugin", "plugins", "putAuthScheme", "appId"); SdkClientBuilder builder = testClientBuilder(); BeanInfo beanInfo = Introspector.getBeanInfo(builder.getClass()); diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/useragent/AppIdResolutionTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/useragent/AppIdResolutionTest.java new file mode 100644 index 000000000000..158302e6540d --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/useragent/AppIdResolutionTest.java @@ -0,0 +1,115 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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.awssdk.core.internal.useragent; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.profiles.ProfileFile; +import software.amazon.awssdk.profiles.ProfileProperty; +import software.amazon.awssdk.testutils.EnvironmentVariableHelper; +import software.amazon.awssdk.utils.Pair; +import software.amazon.awssdk.utils.StringInputStream; +import software.amazon.awssdk.utils.StringUtils; + +class AppIdResolutionTest { + + private static final EnvironmentVariableHelper ENVIRONMENT_VARIABLE_HELPER = new EnvironmentVariableHelper(); + private static final String PROFILE = "test"; + + @AfterEach + public void cleanup() { + ENVIRONMENT_VARIABLE_HELPER.reset(); + System.clearProperty(SdkSystemSetting.AWS_SDK_UA_APP_ID.property()); + } + + @ParameterizedTest(name = "{index} - {0}") + @MethodSource("inputValues") + void resolveAppIdFromEnvironment(String description, String systemProperty, String envVar, + ProfileFile profileFile, String expected) { + + setUpSystemSettings(systemProperty, envVar); + + AppIdResolver resolver = AppIdResolver.create().profileName(PROFILE); + if (profileFile != null) { + resolver.profileFile(() -> profileFile); + } + + if (expected != null) { + assertThat(resolver.resolve()).isNotEmpty().contains(expected); + } else { + assertThat(resolver.resolve()).isEmpty(); + } + } + + private static Stream inputValues() { + ProfileFile emptyProfile = configFile("profile test", Pair.of("foo", "bar")); + + Function testProfileConfig = + s -> configFile("profile test", Pair.of(ProfileProperty.SDK_UA_APP_ID, s)); + + return Stream.of( + Arguments.of("Without input, resolved value is null", null, null, null, null), + Arguments.of("Setting system property only gives result", "SystemPropertyAppId", null, null, "SystemPropertyAppId"), + Arguments.of("Setting env var only gives result", null, "EnvVarAppId", null, "EnvVarAppId"), + Arguments.of("System property takes precedence over env var", "SystemPropertyAppId", "EnvVarAppId", null, + "SystemPropertyAppId"), + Arguments.of("Setting profile file only gives result", null, null, testProfileConfig.apply("profileAppId"), + "profileAppId"), + Arguments.of("When profile file exists but has no input, resolved value is null", null, null, emptyProfile, null), + Arguments.of("System property takes precedence over profile file", "SystemPropertyAppId", null, + testProfileConfig.apply("profileAppId"), "SystemPropertyAppId"), + Arguments.of("Env var takes precedence over profile file", null, "EnvVarAppId", + testProfileConfig.apply("profileAppId"), "EnvVarAppId"), + Arguments.of("System prop var takes precedence over profile file", null, "EnvVarAppId", + testProfileConfig.apply("profileAppId"), "EnvVarAppId") + ); + } + + private static void setUpSystemSettings(String systemProperty, String envVar) { + if (!StringUtils.isEmpty(systemProperty)) { + System.setProperty(SdkSystemSetting.AWS_SDK_UA_APP_ID.property(), systemProperty); + } + if (!StringUtils.isEmpty(envVar)) { + ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_SDK_UA_APP_ID.environmentVariable(), envVar); + } + } + + private static ProfileFile configFile(String name, Pair... pairs) { + String values = Arrays.stream(pairs) + .map(pair -> String.format("%s=%s", pair.left(), pair.right())) + .collect(Collectors.joining(System.lineSeparator())); + String contents = String.format("[%s]\n%s", name, values); + + return configFile(contents); + } + + private static ProfileFile configFile(String credentialFile) { + return ProfileFile.builder() + .content(new StringInputStream(credentialFile)) + .type(ProfileFile.Type.CONFIGURATION) + .build(); + } +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/useragent/SdkUserAgentBuilderTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/useragent/SdkUserAgentBuilderTest.java index d8f9343843a8..9df34a525a5c 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/useragent/SdkUserAgentBuilderTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/useragent/SdkUserAgentBuilderTest.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.core.internal.useragent; import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.APP_ID; import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.HTTP; import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.INTERNAL_METADATA_MARKER; import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.IO; @@ -50,8 +51,8 @@ private static Stream inputValues() { "OpenJDK_64-Bit_Server_VM#21.0.2+13-LTS", "vendor#Amazon.com_Inc.", "en_US", Arrays.asList("Kotlin", "Scala")); - SdkClientUserAgentProperties minimalProperties = sdkProperties(null, null, null, null); - SdkClientUserAgentProperties maximalProperties = sdkProperties("standard", "arbitrary", "async", "Netty"); + SdkClientUserAgentProperties minimalProperties = sdkProperties(null, null, null, null, null); + SdkClientUserAgentProperties maximalProperties = sdkProperties("standard", "arbitrary", "async", "Netty", "someAppId"); return Stream.of( Arguments.of("default sysagent, empty requestvalues", @@ -62,40 +63,47 @@ private static Stream inputValues() { "aws-sdk-java/2.26.22-SNAPSHOT ua/2.0 os/Mac_OS_X#14.6.1 lang/java#21.0.2 " + "md/OpenJDK_64-Bit_Server_VM#21.0.2+13-LTS md/vendor#Amazon.com_Inc. md/en_US md/Kotlin md/Scala " + "exec-env/lambda cfg/retry-mode#standard", - sdkProperties("standard", null, null, null), + sdkProperties("standard", null, null, null, null), maximalSysAgent), Arguments.of("standard sysagent, request values - internalMarker", "aws-sdk-java/2.26.22-SNAPSHOT md/internal ua/2.0 os/Mac_OS_X#14.6.1 lang/java#21.0.2 " + "md/OpenJDK_64-Bit_Server_VM#21.0.2+13-LTS md/vendor#Amazon.com_Inc. md/en_US md/Kotlin md/Scala exec-env/lambda", - sdkProperties(null, "arbitrary", null, null), + sdkProperties(null, "arbitrary", null, null, null), maximalSysAgent), Arguments.of("standard sysagent, request values - io", "aws-sdk-java/2.26.22-SNAPSHOT md/io#async ua/2.0 os/Mac_OS_X#14.6.1 lang/java#21.0.2 " + "md/OpenJDK_64-Bit_Server_VM#21.0.2+13-LTS md/vendor#Amazon.com_Inc. md/en_US md/Kotlin md/Scala exec-env/lambda", - sdkProperties(null, null, "async", null), + sdkProperties(null, null, "async", null, null), maximalSysAgent), Arguments.of("standard sysagent, request values - http", "aws-sdk-java/2.26.22-SNAPSHOT md/http#Apache ua/2.0 os/Mac_OS_X#14.6.1 lang/java#21.0.2 " + "md/OpenJDK_64-Bit_Server_VM#21.0.2+13-LTS md/vendor#Amazon.com_Inc. md/en_US md/Kotlin md/Scala exec-env/lambda", - sdkProperties(null, null, null, "Apache"), + sdkProperties(null, null, null, "Apache", null), maximalSysAgent), Arguments.of("standard sysagent, request values - authSource", "aws-sdk-java/2.26.22-SNAPSHOT ua/2.0 os/Mac_OS_X#14.6.1 lang/java#21.0.2 " + "md/OpenJDK_64-Bit_Server_VM#21.0.2+13-LTS md/vendor#Amazon.com_Inc. md/en_US md/Kotlin md/Scala " + "exec-env/lambda", - sdkProperties(null, null, null, null), + sdkProperties(null, null, null, null, null), + maximalSysAgent), + Arguments.of("standard sysagent, request values - appId", + "aws-sdk-java/2.26.22-SNAPSHOT ua/2.0 os/Mac_OS_X#14.6.1 lang/java#21.0.2 " + + "md/OpenJDK_64-Bit_Server_VM#21.0.2+13-LTS md/vendor#Amazon.com_Inc. md/en_US md/Kotlin md/Scala " + + "exec-env/lambda app/someAppId", + sdkProperties(null, null, null, null, "someAppId"), maximalSysAgent), Arguments.of("standard sysagent, request values - maximal", "aws-sdk-java/2.26.22-SNAPSHOT md/io#async md/http#Netty md/internal ua/2.0 os/Mac_OS_X#14.6.1 " + "lang/java#21.0.2 " + "md/OpenJDK_64-Bit_Server_VM#21.0.2+13-LTS md/vendor#Amazon.com_Inc. md/en_US md/Kotlin md/Scala " - + "exec-env/lambda cfg/retry-mode#standard", + + "exec-env/lambda cfg/retry-mode#standard app/someAppId", maximalProperties, maximalSysAgent) ); } - private static SdkClientUserAgentProperties sdkProperties(String retryMode, String internalMarker, String io, String http) { + private static SdkClientUserAgentProperties sdkProperties(String retryMode, String internalMarker, String io, + String http, String appId) { SdkClientUserAgentProperties properties = new SdkClientUserAgentProperties(); if (retryMode != null) { @@ -114,6 +122,10 @@ private static SdkClientUserAgentProperties sdkProperties(String retryMode, Stri properties.putProperty(HTTP, http); } + if (appId != null) { + properties.putProperty(APP_ID, appId); + } + return properties; } diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/useragent/AppIdUserAgentTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/useragent/AppIdUserAgentTest.java new file mode 100644 index 000000000000..58ff279b1a30 --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/useragent/AppIdUserAgentTest.java @@ -0,0 +1,119 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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.awssdk.services.useragent; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.interceptor.Context; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.restjsonendpointproviders.RestJsonEndpointProvidersClient; +import software.amazon.awssdk.services.restjsonendpointproviders.RestJsonEndpointProvidersClientBuilder; +import software.amazon.awssdk.utils.StringUtils; + +class AppIdUserAgentTest { + private CapturingInterceptor interceptor; + + private static final String USER_AGENT_HEADER_NAME = "User-Agent"; + + @BeforeEach + public void setup() { + this.interceptor = new CapturingInterceptor(); + } + + @AfterEach + public void cleanup() { + System.clearProperty(SdkSystemSetting.AWS_SDK_UA_APP_ID.property()); + } + + @ParameterizedTest(name = "{index} - {0}") + @MethodSource("inputValues") + void resolveAppIdFromEnvironment(String description, String clientAppId, String systemProperty, String expected) { + if (!StringUtils.isEmpty(systemProperty)) { + System.setProperty(SdkSystemSetting.AWS_SDK_UA_APP_ID.property(), systemProperty); + } + + RestJsonEndpointProvidersClientBuilder clientBuilder = syncClientBuilder(); + + if (!StringUtils.isEmpty(clientAppId)) { + ClientOverrideConfiguration config = clientBuilder.overrideConfiguration().toBuilder().appId(clientAppId).build(); + clientBuilder.overrideConfiguration(config); + } + + assertThatThrownBy(() -> clientBuilder.build().allTypes(r -> {})) + .hasMessageContaining("stop"); + + Map> headers = interceptor.context.httpRequest().headers(); + assertThat(headers).containsKey(USER_AGENT_HEADER_NAME); + String userAgent = headers.get(USER_AGENT_HEADER_NAME).get(0); + + if (expected != null) { + assertThat(userAgent).contains("app/" + expected); + } else { + assertThat(userAgent).doesNotContain("app/"); + } + } + + private static Stream inputValues() { + return Stream.of( + Arguments.of("Without appId input, nothing is added to user agent", null, null, null), + Arguments.of("Values resolved from environment are propagated to user agent", null, + "SystemPropertyAppId", "SystemPropertyAppId"), + Arguments.of("Client value is propagated to user agent", "ClientAppId", null, "ClientAppId"), + Arguments.of("Client value takes precedence over environment values", "ClientAppId", "SystemPropertyAppId", + "ClientAppId") + ); + } + + private RestJsonEndpointProvidersClientBuilder syncClientBuilder() { + return RestJsonEndpointProvidersClient.builder() + .region(Region.US_WEST_2) + .credentialsProvider( + StaticCredentialsProvider.create( + AwsBasicCredentials.create("akid", "skid"))) + .overrideConfiguration(c -> c.addExecutionInterceptor(interceptor)); + } + + public static class CapturingInterceptor implements ExecutionInterceptor { + private Context.BeforeTransmission context; + private ExecutionAttributes executionAttributes; + + @Override + public void beforeTransmission(Context.BeforeTransmission context, ExecutionAttributes executionAttributes) { + this.context = context; + this.executionAttributes = executionAttributes; + throw new RuntimeException("stop"); + } + + public ExecutionAttributes executionAttributes() { + return executionAttributes; + } + } +} diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/customizeduseragent/InternalUserAgentTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/useragent/InternalUserAgentTest.java similarity index 98% rename from test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/customizeduseragent/InternalUserAgentTest.java rename to test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/useragent/InternalUserAgentTest.java index ccb2f28f8b10..8462ff843a84 100644 --- a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/customizeduseragent/InternalUserAgentTest.java +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/useragent/InternalUserAgentTest.java @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -package software.amazon.awssdk.services.customizeduseragent; +package software.amazon.awssdk.services.useragent; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;