diff --git a/.changes/2e8e4fee-c462-45d2-b421-46520d06eb60.json b/.changes/2e8e4fee-c462-45d2-b421-46520d06eb60.json new file mode 100644 index 00000000000..69b9eb6dc31 --- /dev/null +++ b/.changes/2e8e4fee-c462-45d2-b421-46520d06eb60.json @@ -0,0 +1,8 @@ +{ + "id": "2e8e4fee-c462-45d2-b421-46520d06eb60", + "type": "feature", + "description": "Add new sources for User-Agent app id", + "issues": [ + "awslabs/aws-sdk-kotlin#945" + ] +} \ No newline at end of file diff --git a/aws-runtime/aws-config/api/aws-config.api b/aws-runtime/aws-config/api/aws-config.api index 0f62a04a1ea..b92308801af 100644 --- a/aws-runtime/aws-config/api/aws-config.api +++ b/aws-runtime/aws-config/api/aws-config.api @@ -315,6 +315,9 @@ public final class aws/sdk/kotlin/runtime/config/profile/AwsSharedConfigKt { public final class aws/sdk/kotlin/runtime/config/retries/ResolveRetryStrategyKt { } +public final class aws/sdk/kotlin/runtime/config/useragent/ResolveUserAgentKt { +} + public abstract interface class aws/sdk/kotlin/runtime/region/RegionProvider { public abstract fun getRegion (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } diff --git a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/AbstractAwsSdkClientFactory.kt b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/AbstractAwsSdkClientFactory.kt index d5cfd9f834f..f5986ac8408 100644 --- a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/AbstractAwsSdkClientFactory.kt +++ b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/AbstractAwsSdkClientFactory.kt @@ -11,6 +11,7 @@ import aws.sdk.kotlin.runtime.config.endpoints.resolveUseFips import aws.sdk.kotlin.runtime.config.profile.AwsSharedConfig import aws.sdk.kotlin.runtime.config.profile.loadAwsSharedConfig import aws.sdk.kotlin.runtime.config.retries.resolveRetryStrategy +import aws.sdk.kotlin.runtime.config.useragent.resolveUserAgentAppId import aws.sdk.kotlin.runtime.region.resolveRegion import aws.smithy.kotlin.runtime.ExperimentalApi import aws.smithy.kotlin.runtime.client.RetryStrategyClientConfig @@ -57,7 +58,8 @@ public abstract class AbstractAwsSdkClientFactory< val tracer = telemetryProvider.tracerProvider.getOrCreateTracer("AwsSdkClientFactory") tracer.withSpan("fromEnvironment") { - val sharedConfig = asyncLazy { loadAwsSharedConfig(PlatformProvider.System) } + val platform = PlatformProvider.System + val sharedConfig = asyncLazy { loadAwsSharedConfig(platform) } val profile = asyncLazy { sharedConfig.get().activeProfile } // As a DslBuilderProperty, the value of retryStrategy cannot be checked for nullability because it may have @@ -68,10 +70,12 @@ public abstract class AbstractAwsSdkClientFactory< block?.let(config::apply) - config.logMode = config.logMode ?: ClientSettings.LogMode.resolve(platform = PlatformProvider.System) + config.logMode = config.logMode ?: ClientSettings.LogMode.resolve(platform = platform) config.region = config.region ?: resolveRegion(profile = profile) config.useFips = config.useFips ?: resolveUseFips(profile = profile) config.useDualStack = config.useDualStack ?: resolveUseDualStack(profile = profile) + config.applicationId = config.applicationId ?: resolveUserAgentAppId(platform, profile) + finalizeConfig(builder, sharedConfig) } return builder.build() diff --git a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/AwsSdkSetting.kt b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/AwsSdkSetting.kt index 875cdc06a60..e0ac6ec6265 100644 --- a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/AwsSdkSetting.kt +++ b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/AwsSdkSetting.kt @@ -6,6 +6,8 @@ package aws.sdk.kotlin.runtime.config import aws.sdk.kotlin.runtime.InternalSdkApi +import aws.sdk.kotlin.runtime.http.AWS_APP_ID_ENV +import aws.sdk.kotlin.runtime.http.AWS_APP_ID_PROP import aws.smithy.kotlin.runtime.client.config.RetryMode import aws.smithy.kotlin.runtime.config.* import aws.smithy.kotlin.runtime.net.Url @@ -45,6 +47,11 @@ public object AwsSdkSetting { */ public val AwsRegion: EnvironmentSetting = strEnvSetting("aws.region", "AWS_REGION") + /** + * Configure the user agent app ID + */ + public val AwsAppId: EnvironmentSetting = strEnvSetting(AWS_APP_ID_PROP, AWS_APP_ID_ENV) + /** * Configure the default path to the shared config file. */ diff --git a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/profile/AwsProfile.kt b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/profile/AwsProfile.kt index 48cba4272c9..33a3753c0ef 100644 --- a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/profile/AwsProfile.kt +++ b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/profile/AwsProfile.kt @@ -122,6 +122,13 @@ public val AwsProfile.ignoreEndpointUrls: Boolean? public val AwsProfile.servicesSection: String? get() = getOrNull("services") +/** + * The SDK user agent app ID used to identify applications. + */ +@InternalSdkApi +public val AwsProfile.sdkUserAgentAppId: String? + get() = getOrNull("sdk_ua_app_id") + /** * Parse a config value as a boolean, ignoring case. */ diff --git a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/useragent/ResolveUserAgent.kt b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/useragent/ResolveUserAgent.kt new file mode 100644 index 00000000000..0c71e55d179 --- /dev/null +++ b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/useragent/ResolveUserAgent.kt @@ -0,0 +1,22 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.sdk.kotlin.runtime.config.useragent + +import aws.sdk.kotlin.runtime.InternalSdkApi +import aws.sdk.kotlin.runtime.config.AwsSdkSetting +import aws.sdk.kotlin.runtime.config.profile.AwsProfile +import aws.sdk.kotlin.runtime.config.profile.sdkUserAgentAppId +import aws.smithy.kotlin.runtime.config.resolve +import aws.smithy.kotlin.runtime.util.LazyAsyncValue +import aws.smithy.kotlin.runtime.util.PlatformProvider + +/** + * Attempts to resolve user agent from specified sources. + * @return The user agent app id if found, null if not + */ +@InternalSdkApi +public suspend fun resolveUserAgentAppId(platform: PlatformProvider = PlatformProvider.System, profile: LazyAsyncValue): String? = + AwsSdkSetting.AwsAppId.resolve(platform) ?: profile.get().sdkUserAgentAppId diff --git a/aws-runtime/aws-config/jvm/test/aws/sdk/kotlin/runtime/config/AbstractAwsSdkClientFactoryTest.kt b/aws-runtime/aws-config/jvm/test/aws/sdk/kotlin/runtime/config/AbstractAwsSdkClientFactoryTest.kt index 725794d36dc..daf64bcb56b 100644 --- a/aws-runtime/aws-config/jvm/test/aws/sdk/kotlin/runtime/config/AbstractAwsSdkClientFactoryTest.kt +++ b/aws-runtime/aws-config/jvm/test/aws/sdk/kotlin/runtime/config/AbstractAwsSdkClientFactoryTest.kt @@ -6,15 +6,31 @@ package aws.sdk.kotlin.runtime.config import aws.sdk.kotlin.runtime.client.AwsSdkClientConfig +import aws.sdk.kotlin.runtime.config.profile.loadAwsSharedConfig +import aws.sdk.kotlin.runtime.config.useragent.resolveUserAgentAppId +import aws.sdk.kotlin.runtime.config.utils.mockPlatform import aws.smithy.kotlin.runtime.client.* import aws.smithy.kotlin.runtime.retries.StandardRetryStrategy +import aws.smithy.kotlin.runtime.util.PlatformProvider +import aws.smithy.kotlin.runtime.util.asyncLazy +import io.kotest.extensions.system.withEnvironment import io.kotest.extensions.system.withSystemProperties import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path +import kotlin.io.path.absolutePathString +import kotlin.io.path.deleteIfExists +import kotlin.io.path.writeText import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertIs +import kotlin.time.Duration.Companion.seconds class AbstractAwsSdkClientFactoryTest { + @JvmField + @TempDir + var tempDir: Path? = null + @Test fun testFromEnvironmentFavorsExplicitConfig() = runTest { val explicitRegion = "explicit-region" @@ -41,6 +57,53 @@ class AbstractAwsSdkClientFactoryTest { assertIs(client.config.retryStrategy) } } + + @Test + fun testFromEnvironmentResolvesAppId() = runTest( + timeout = 20.seconds, + ) { + val credentialsFile = tempDir!!.resolve("credentials") + val configFile = tempDir!!.resolve("config") + + configFile.writeText("[profile foo]\nsdk_ua_app_id = profile-app-id") + + val testPlatform = mockPlatform( + pathSegment = PlatformProvider.System.filePathSeparator, + awsProfileEnv = "foo", + homeEnv = "/home/user", + awsConfigFileEnv = configFile.absolutePathString(), + awsSharedCredentialsFileEnv = credentialsFile.absolutePathString(), + os = PlatformProvider.System.osInfo(), + ) + + val sharedConfig = asyncLazy { loadAwsSharedConfig(testPlatform) } + val profile = asyncLazy { sharedConfig.get().activeProfile } + + assertEquals("profile-app-id", resolveUserAgentAppId(testPlatform, profile)) + + configFile.deleteIfExists() + credentialsFile.deleteIfExists() + + withEnvironment( + mapOf( + AwsSdkSetting.AwsAppId.envVar to "env-app-id", + ), + ) { + assertEquals("env-app-id", TestClient.fromEnvironment().config.applicationId) + + withSystemProperties( + mapOf( + AwsSdkSetting.AwsAppId.sysProp to "system-properties-app-id", + ), + ) { + assertEquals("system-properties-app-id", TestClient.fromEnvironment().config.applicationId) + assertEquals( + "explicit-app-id", + TestClient.fromEnvironment { applicationId = "explicit-app-id" }.config.applicationId, + ) + } + } + } } private interface TestClient : SdkClient { @@ -59,9 +122,10 @@ private interface TestClient : SdkClient { class Config private constructor(builder: Builder) : SdkClientConfig, AwsSdkClientConfig, RetryStrategyClientConfig by builder.buildRetryStrategyClientConfig() { override val clientName: String = builder.clientName override val logMode: LogMode = builder.logMode ?: LogMode.Default - override val region: String = builder.region ?: error("region is required") + override val region: String? = builder.region override var useFips: Boolean = builder.useFips ?: false override var useDualStack: Boolean = builder.useDualStack ?: false + override val applicationId: String? = builder.applicationId // new: inherits builder equivalents for Config base classes class Builder : AwsSdkClientConfig.Builder, SdkClientConfig.Builder, RetryStrategyClientConfig.Builder by RetryStrategyClientConfigImpl.BuilderImpl() { @@ -70,6 +134,7 @@ private interface TestClient : SdkClient { override var region: String? = null override var useFips: Boolean? = null override var useDualStack: Boolean? = null + override var applicationId: String? = null override fun build(): Config = Config(this) } } diff --git a/aws-runtime/aws-config/jvm/test/aws/sdk/kotlin/runtime/config/profile/AWSConfigLoaderFilesystemTest.kt b/aws-runtime/aws-config/jvm/test/aws/sdk/kotlin/runtime/config/profile/AWSConfigLoaderFilesystemTest.kt index e91280a3bb0..45852d6644e 100644 --- a/aws-runtime/aws-config/jvm/test/aws/sdk/kotlin/runtime/config/profile/AWSConfigLoaderFilesystemTest.kt +++ b/aws-runtime/aws-config/jvm/test/aws/sdk/kotlin/runtime/config/profile/AWSConfigLoaderFilesystemTest.kt @@ -5,16 +5,11 @@ package aws.sdk.kotlin.runtime.config.profile -import aws.smithy.kotlin.runtime.util.OperatingSystem +import aws.sdk.kotlin.runtime.config.utils.mockPlatform import aws.smithy.kotlin.runtime.util.PlatformProvider -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import io.mockk.slot import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir -import java.io.File import java.nio.file.Path import kotlin.io.path.absolutePathString import kotlin.io.path.deleteIfExists @@ -81,51 +76,4 @@ class AWSConfigLoaderFilesystemTest { configFile.deleteIfExists() credentialsFile.deleteIfExists() } - - private fun mockPlatform( - pathSegment: String, - awsProfileEnv: String? = null, - awsConfigFileEnv: String? = null, - homeEnv: String? = null, - awsSharedCredentialsFileEnv: String? = null, - homeProp: String? = null, - os: OperatingSystem, - ): PlatformProvider { - val testPlatform = mockk() - val envKeyParam = slot() - val propKeyParam = slot() - val filePath = slot() - - every { testPlatform.filePathSeparator } returns pathSegment - every { testPlatform.getenv(capture(envKeyParam)) } answers { - when (envKeyParam.captured) { - "AWS_PROFILE" -> awsProfileEnv - "AWS_CONFIG_FILE" -> awsConfigFileEnv - "HOME" -> homeEnv - "AWS_SHARED_CREDENTIALS_FILE" -> awsSharedCredentialsFileEnv - else -> error(envKeyParam.captured) - } - } - every { testPlatform.getProperty(capture(propKeyParam)) } answers { - if (propKeyParam.captured == "user.home") homeProp else null - } - every { testPlatform.osInfo() } returns os - coEvery { - testPlatform.readFileOrNull(capture(filePath)) - } answers { - if (awsConfigFileEnv != null) { - val file = if (filePath.captured.endsWith("config")) { - File(awsConfigFileEnv) - } else { - File(awsSharedCredentialsFileEnv) - } - - if (file.exists()) file.readBytes() else null - } else { - null - } - } - - return testPlatform - } } diff --git a/aws-runtime/aws-config/jvm/test/aws/sdk/kotlin/runtime/config/utils/MockPlatform.kt b/aws-runtime/aws-config/jvm/test/aws/sdk/kotlin/runtime/config/utils/MockPlatform.kt new file mode 100644 index 00000000000..ca0fb3cf4a0 --- /dev/null +++ b/aws-runtime/aws-config/jvm/test/aws/sdk/kotlin/runtime/config/utils/MockPlatform.kt @@ -0,0 +1,63 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.sdk.kotlin.runtime.config.utils + +import aws.smithy.kotlin.runtime.util.OperatingSystem +import aws.smithy.kotlin.runtime.util.PlatformProvider +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import java.io.File + +internal fun mockPlatform( + pathSegment: String, + awsProfileEnv: String? = null, + awsConfigFileEnv: String? = null, + homeEnv: String? = null, + awsSharedCredentialsFileEnv: String? = null, + awsSdkUserAgentAppIdEnv: String? = null, + homeProp: String? = null, + os: OperatingSystem, +): PlatformProvider { + val testPlatform = mockk() + val envKeyParam = slot() + val propKeyParam = slot() + val filePath = slot() + + every { testPlatform.filePathSeparator } returns pathSegment + every { testPlatform.getenv(capture(envKeyParam)) } answers { + when (envKeyParam.captured) { + "AWS_PROFILE" -> awsProfileEnv + "AWS_CONFIG_FILE" -> awsConfigFileEnv + "HOME" -> homeEnv + "AWS_SHARED_CREDENTIALS_FILE" -> awsSharedCredentialsFileEnv + "AWS_SDK_UA_APP_ID" -> awsSdkUserAgentAppIdEnv + else -> error(envKeyParam.captured) + } + } + every { testPlatform.getProperty(capture(propKeyParam)) } answers { + if (propKeyParam.captured == "user.home") homeProp else null + } + every { testPlatform.osInfo() } returns os + coEvery { + testPlatform.readFileOrNull(capture(filePath)) + } answers { + if (awsConfigFileEnv != null) { + val file = if (filePath.captured.endsWith("config")) { + File(awsConfigFileEnv) + } else { + File(awsSharedCredentialsFileEnv) + } + + if (file.exists()) file.readBytes() else null + } else { + null + } + } + + return testPlatform +} diff --git a/aws-runtime/aws-core/api/aws-core.api b/aws-runtime/aws-core/api/aws-core.api index a9a16afdca0..e4b891b34ee 100644 --- a/aws-runtime/aws-core/api/aws-core.api +++ b/aws-runtime/aws-core/api/aws-core.api @@ -38,15 +38,18 @@ public final class aws/sdk/kotlin/runtime/client/AwsClientOption { } public abstract interface class aws/sdk/kotlin/runtime/client/AwsSdkClientConfig : aws/smithy/kotlin/runtime/client/SdkClientConfig { + public abstract fun getApplicationId ()Ljava/lang/String; public abstract fun getRegion ()Ljava/lang/String; public abstract fun getUseDualStack ()Z public abstract fun getUseFips ()Z } public abstract interface class aws/sdk/kotlin/runtime/client/AwsSdkClientConfig$Builder { + public abstract fun getApplicationId ()Ljava/lang/String; public abstract fun getRegion ()Ljava/lang/String; public abstract fun getUseDualStack ()Ljava/lang/Boolean; public abstract fun getUseFips ()Ljava/lang/Boolean; + public abstract fun setApplicationId (Ljava/lang/String;)V public abstract fun setRegion (Ljava/lang/String;)V public abstract fun setUseDualStack (Ljava/lang/Boolean;)V public abstract fun setUseFips (Ljava/lang/Boolean;)V diff --git a/aws-runtime/aws-core/common/src/aws/sdk/kotlin/runtime/client/AwsSdkClientConfig.kt b/aws-runtime/aws-core/common/src/aws/sdk/kotlin/runtime/client/AwsSdkClientConfig.kt index 1849b79923a..b8073748e0e 100644 --- a/aws-runtime/aws-core/common/src/aws/sdk/kotlin/runtime/client/AwsSdkClientConfig.kt +++ b/aws-runtime/aws-core/common/src/aws/sdk/kotlin/runtime/client/AwsSdkClientConfig.kt @@ -32,6 +32,20 @@ public interface AwsSdkClientConfig : SdkClientConfig { */ public val useDualStack: Boolean + /** + * An optional application specific identifier. + * When set it will be appended to the User-Agent header of every request in the form of: `app/{applicationId}`. + * When not explicitly set, the value will be loaded from the following locations: + * + * - JVM System Property: `aws.userAgentAppId` + * - Environment variable: `AWS_SDK_UA_APP_ID` + * - Shared configuration profile attribute: `sdk_ua_app_id` + * + * See [shared configuration settings](https://docs.aws.amazon.com/sdkref/latest/guide/settings-reference.html) + * reference for more information on environment variables and shared config settings. + */ + public val applicationId: String? + public interface Builder { /** * The AWS region (e.g. `us-west-2`) to make requests to. See about AWS @@ -52,5 +66,19 @@ public interface AwsSdkClientConfig : SdkClientConfig { * Disabled by default. */ public var useDualStack: Boolean? + + /** + * An optional application specific identifier. + * When set it will be appended to the User-Agent header of every request in the form of: `app/{applicationId}`. + * When not explicitly set, the value will be loaded from the following locations: + * + * - JVM System Property: `aws.userAgentAppId` + * - Environment variable: `AWS_SDK_UA_APP_ID` + * - Shared configuration profile attribute: `sdk_ua_app_id` + * + * See [shared configuration settings](https://docs.aws.amazon.com/sdkref/latest/guide/settings-reference.html) + * reference for more information on environment variables and shared config settings. + */ + public var applicationId: String? } } diff --git a/aws-runtime/aws-http/api/aws-http.api b/aws-runtime/aws-http/api/aws-http.api index dd30c542798..bc37aa1159a 100644 --- a/aws-runtime/aws-http/api/aws-http.api +++ b/aws-runtime/aws-http/api/aws-http.api @@ -28,7 +28,13 @@ public final class aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata { } public final class aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata$Companion { - public final fun fromEnvironment (Laws/sdk/kotlin/runtime/http/ApiMetadata;)Laws/sdk/kotlin/runtime/http/AwsUserAgentMetadata; + public final fun fromEnvironment (Laws/sdk/kotlin/runtime/http/ApiMetadata;Ljava/lang/String;)Laws/sdk/kotlin/runtime/http/AwsUserAgentMetadata; + public static synthetic fun fromEnvironment$default (Laws/sdk/kotlin/runtime/http/AwsUserAgentMetadata$Companion;Laws/sdk/kotlin/runtime/http/ApiMetadata;Ljava/lang/String;ILjava/lang/Object;)Laws/sdk/kotlin/runtime/http/AwsUserAgentMetadata; +} + +public final class aws/sdk/kotlin/runtime/http/AwsUserAgentMetadataKt { + public static final field AWS_APP_ID_ENV Ljava/lang/String; + public static final field AWS_APP_ID_PROP Ljava/lang/String; } public final class aws/sdk/kotlin/runtime/http/operation/CustomUserAgentMetadata { diff --git a/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata.kt b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata.kt index 12c69098f0b..c1ffdeffc9b 100644 --- a/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata.kt +++ b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata.kt @@ -13,10 +13,10 @@ import aws.smithy.kotlin.runtime.util.* import kotlin.jvm.JvmInline internal const val AWS_EXECUTION_ENV = "AWS_EXECUTION_ENV" -internal const val AWS_APP_ID_ENV = "AWS_SDK_UA_APP_ID" +public const val AWS_APP_ID_ENV: String = "AWS_SDK_UA_APP_ID" // non-standard environment variables/properties -internal const val AWS_APP_ID_PROP = "aws.userAgentAppId" +public const val AWS_APP_ID_PROP: String = "aws.userAgentAppId" internal const val FRAMEWORK_METADATA_ENV = "AWS_FRAMEWORK_METADATA" internal const val FRAMEWORK_METADATA_PROP = "aws.frameworkMetadata" @@ -39,7 +39,8 @@ public data class AwsUserAgentMetadata( */ public fun fromEnvironment( apiMeta: ApiMetadata, - ): AwsUserAgentMetadata = loadAwsUserAgentMetadataFromEnvironment(PlatformProvider.System, apiMeta) + appId: String? = null, + ): AwsUserAgentMetadata = loadAwsUserAgentMetadataFromEnvironment(PlatformProvider.System, apiMeta, appId) } /** @@ -87,12 +88,12 @@ public data class AwsUserAgentMetadata( get() = "$sdkMetadata" } -internal fun loadAwsUserAgentMetadataFromEnvironment(platform: PlatformProvider, apiMeta: ApiMetadata): AwsUserAgentMetadata { +internal fun loadAwsUserAgentMetadataFromEnvironment(platform: PlatformProvider, apiMeta: ApiMetadata, appIdValue: String? = null): AwsUserAgentMetadata { val sdkMeta = SdkMetadata("kotlin", apiMeta.version) val osInfo = platform.osInfo() val osMetadata = OsMetadata(osInfo.family, osInfo.version) val langMeta = platformLanguageMetadata() - val appId = platform.getProperty(AWS_APP_ID_PROP) ?: platform.getenv(AWS_APP_ID_ENV) + val appId = appIdValue ?: platform.getProperty(AWS_APP_ID_PROP) ?: platform.getenv(AWS_APP_ID_ENV) val frameworkMetadata = FrameworkMetadata.fromEnvironment(platform) val customMetadata = CustomUserAgentMetadata.fromEnvironment(platform) diff --git a/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadataTest.kt b/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadataTest.kt index c4ca4da4315..2cc28792ca2 100644 --- a/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadataTest.kt +++ b/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadataTest.kt @@ -109,4 +109,27 @@ class AwsUserAgentMetadataTest { actual.xAmzUserAgent.shouldContain(test.expected) } } + + @Test + fun testExplicitAppId() { + val testEnvironments = listOf( + EnvironmentTest( + TestPlatformProvider( + env = mapOf(AWS_APP_ID_ENV to "app-id-1"), + ), + "app/explicit-app-id", + ), + EnvironmentTest( + TestPlatformProvider( + env = mapOf(AWS_APP_ID_ENV to "app-id-1"), + props = mapOf(AWS_APP_ID_PROP to "app-id-2"), + ), + "app/explicit-app-id", + ), + ) + testEnvironments.forEach { test -> + val actual = loadAwsUserAgentMetadataFromEnvironment(test.provider, ApiMetadata("Test Service", "1.2.3"), "explicit-app-id") + actual.xAmzUserAgent.shouldContain(test.expected) + } + } } diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsServiceConfigIntegration.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsServiceConfigIntegration.kt index cfa36194033..1f9ca20f798 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsServiceConfigIntegration.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsServiceConfigIntegration.kt @@ -33,6 +33,26 @@ class AwsServiceConfigIntegration : KotlinIntegration { order = -100 } + val UserAgentAppId: ConfigProperty = ConfigProperty { + name = "applicationId" + symbol = KotlinTypes.String.asNullable() + baseClass = AwsRuntimeTypes.Core.Client.AwsSdkClientConfig + useNestedBuilderBaseClass() + documentation = """ + An optional application specific identifier. + When set it will be appended to the User-Agent header of every request in the form of: `app/{applicationId}`. + When not explicitly set, the value will be loaded from the following locations: + + - JVM System Property: `aws.userAgentAppId` + - Environment variable: `AWS_SDK_UA_APP_ID` + - Shared configuration profile attribute: `sdk_ua_app_id` + + See [shared configuration settings](https://docs.aws.amazon.com/sdkref/latest/guide/settings-reference.html) + reference for more information on environment variables and shared config settings. + """.trimIndent() + order = 100 + } + // override the credentials provider prop registered by the Sigv4AuthSchemeIntegration, updates the // documentation and sets a default value for AWS SDK to the default chain. val CredentialsProviderProp: ConfigProperty = ConfigProperty { @@ -158,5 +178,6 @@ class AwsServiceConfigIntegration : KotlinIntegration { add(UseDualStackProp) add(EndpointUrlProp) add(AwsRetryPolicy) + add(UserAgentAppId) } } diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/protocols/middleware/UserAgentMiddleware.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/protocols/middleware/UserAgentMiddleware.kt index 74ff72d77e3..0cd1a9fd03a 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/protocols/middleware/UserAgentMiddleware.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/protocols/middleware/UserAgentMiddleware.kt @@ -39,7 +39,11 @@ class UserAgentMiddleware : ProtocolMiddleware { writer.addImport(uaSymbol) writer.addImport(apiMetaSymbol) writer.addImport(middlewareSymbol) - writer.write("private val awsUserAgentMetadata = #T.fromEnvironment(#T(ServiceId, SdkVersion))", uaSymbol, apiMetaSymbol) + writer.write( + "private val awsUserAgentMetadata = #T.fromEnvironment(#T(ServiceId, SdkVersion), config.applicationId)", + uaSymbol, + apiMetaSymbol, + ) } override fun render(ctx: ProtocolGenerator.GenerationContext, op: OperationShape, writer: KotlinWriter) {