Skip to content

Commit 24851dc

Browse files
authored
feat: add more user agent app id sources (#1071)
1 parent 3d1d564 commit 24851dc

File tree

16 files changed

+276
-63
lines changed

16 files changed

+276
-63
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "2e8e4fee-c462-45d2-b421-46520d06eb60",
3+
"type": "feature",
4+
"description": "Add new sources for User-Agent app id",
5+
"issues": [
6+
"awslabs/aws-sdk-kotlin#945"
7+
]
8+
}

aws-runtime/aws-config/api/aws-config.api

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,9 @@ public final class aws/sdk/kotlin/runtime/config/profile/AwsSharedConfigKt {
315315
public final class aws/sdk/kotlin/runtime/config/retries/ResolveRetryStrategyKt {
316316
}
317317

318+
public final class aws/sdk/kotlin/runtime/config/useragent/ResolveUserAgentKt {
319+
}
320+
318321
public abstract interface class aws/sdk/kotlin/runtime/region/RegionProvider {
319322
public abstract fun getRegion (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
320323
}

aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/AbstractAwsSdkClientFactory.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import aws.sdk.kotlin.runtime.config.endpoints.resolveUseFips
1111
import aws.sdk.kotlin.runtime.config.profile.AwsSharedConfig
1212
import aws.sdk.kotlin.runtime.config.profile.loadAwsSharedConfig
1313
import aws.sdk.kotlin.runtime.config.retries.resolveRetryStrategy
14+
import aws.sdk.kotlin.runtime.config.useragent.resolveUserAgentAppId
1415
import aws.sdk.kotlin.runtime.region.resolveRegion
1516
import aws.smithy.kotlin.runtime.ExperimentalApi
1617
import aws.smithy.kotlin.runtime.client.RetryStrategyClientConfig
@@ -57,7 +58,8 @@ public abstract class AbstractAwsSdkClientFactory<
5758
val tracer = telemetryProvider.tracerProvider.getOrCreateTracer("AwsSdkClientFactory")
5859

5960
tracer.withSpan("fromEnvironment") {
60-
val sharedConfig = asyncLazy { loadAwsSharedConfig(PlatformProvider.System) }
61+
val platform = PlatformProvider.System
62+
val sharedConfig = asyncLazy { loadAwsSharedConfig(platform) }
6163
val profile = asyncLazy { sharedConfig.get().activeProfile }
6264

6365
// As a DslBuilderProperty, the value of retryStrategy cannot be checked for nullability because it may have
@@ -68,10 +70,12 @@ public abstract class AbstractAwsSdkClientFactory<
6870

6971
block?.let(config::apply)
7072

71-
config.logMode = config.logMode ?: ClientSettings.LogMode.resolve(platform = PlatformProvider.System)
73+
config.logMode = config.logMode ?: ClientSettings.LogMode.resolve(platform = platform)
7274
config.region = config.region ?: resolveRegion(profile = profile)
7375
config.useFips = config.useFips ?: resolveUseFips(profile = profile)
7476
config.useDualStack = config.useDualStack ?: resolveUseDualStack(profile = profile)
77+
config.applicationId = config.applicationId ?: resolveUserAgentAppId(platform, profile)
78+
7579
finalizeConfig(builder, sharedConfig)
7680
}
7781
return builder.build()

aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/AwsSdkSetting.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
package aws.sdk.kotlin.runtime.config
77

88
import aws.sdk.kotlin.runtime.InternalSdkApi
9+
import aws.sdk.kotlin.runtime.http.AWS_APP_ID_ENV
10+
import aws.sdk.kotlin.runtime.http.AWS_APP_ID_PROP
911
import aws.smithy.kotlin.runtime.client.config.RetryMode
1012
import aws.smithy.kotlin.runtime.config.*
1113
import aws.smithy.kotlin.runtime.net.Url
@@ -45,6 +47,11 @@ public object AwsSdkSetting {
4547
*/
4648
public val AwsRegion: EnvironmentSetting<String> = strEnvSetting("aws.region", "AWS_REGION")
4749

50+
/**
51+
* Configure the user agent app ID
52+
*/
53+
public val AwsAppId: EnvironmentSetting<String> = strEnvSetting(AWS_APP_ID_PROP, AWS_APP_ID_ENV)
54+
4855
/**
4956
* Configure the default path to the shared config file.
5057
*/

aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/profile/AwsProfile.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,13 @@ public val AwsProfile.ignoreEndpointUrls: Boolean?
122122
public val AwsProfile.servicesSection: String?
123123
get() = getOrNull("services")
124124

125+
/**
126+
* The SDK user agent app ID used to identify applications.
127+
*/
128+
@InternalSdkApi
129+
public val AwsProfile.sdkUserAgentAppId: String?
130+
get() = getOrNull("sdk_ua_app_id")
131+
125132
/**
126133
* Parse a config value as a boolean, ignoring case.
127134
*/
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package aws.sdk.kotlin.runtime.config.useragent
7+
8+
import aws.sdk.kotlin.runtime.InternalSdkApi
9+
import aws.sdk.kotlin.runtime.config.AwsSdkSetting
10+
import aws.sdk.kotlin.runtime.config.profile.AwsProfile
11+
import aws.sdk.kotlin.runtime.config.profile.sdkUserAgentAppId
12+
import aws.smithy.kotlin.runtime.config.resolve
13+
import aws.smithy.kotlin.runtime.util.LazyAsyncValue
14+
import aws.smithy.kotlin.runtime.util.PlatformProvider
15+
16+
/**
17+
* Attempts to resolve user agent from specified sources.
18+
* @return The user agent app id if found, null if not
19+
*/
20+
@InternalSdkApi
21+
public suspend fun resolveUserAgentAppId(platform: PlatformProvider = PlatformProvider.System, profile: LazyAsyncValue<AwsProfile>): String? =
22+
AwsSdkSetting.AwsAppId.resolve(platform) ?: profile.get().sdkUserAgentAppId

aws-runtime/aws-config/jvm/test/aws/sdk/kotlin/runtime/config/AbstractAwsSdkClientFactoryTest.kt

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,31 @@
66
package aws.sdk.kotlin.runtime.config
77

88
import aws.sdk.kotlin.runtime.client.AwsSdkClientConfig
9+
import aws.sdk.kotlin.runtime.config.profile.loadAwsSharedConfig
10+
import aws.sdk.kotlin.runtime.config.useragent.resolveUserAgentAppId
11+
import aws.sdk.kotlin.runtime.config.utils.mockPlatform
912
import aws.smithy.kotlin.runtime.client.*
1013
import aws.smithy.kotlin.runtime.retries.StandardRetryStrategy
14+
import aws.smithy.kotlin.runtime.util.PlatformProvider
15+
import aws.smithy.kotlin.runtime.util.asyncLazy
16+
import io.kotest.extensions.system.withEnvironment
1117
import io.kotest.extensions.system.withSystemProperties
1218
import kotlinx.coroutines.test.runTest
19+
import org.junit.jupiter.api.io.TempDir
20+
import java.nio.file.Path
21+
import kotlin.io.path.absolutePathString
22+
import kotlin.io.path.deleteIfExists
23+
import kotlin.io.path.writeText
1324
import kotlin.test.Test
1425
import kotlin.test.assertEquals
1526
import kotlin.test.assertIs
27+
import kotlin.time.Duration.Companion.seconds
1628

1729
class AbstractAwsSdkClientFactoryTest {
30+
@JvmField
31+
@TempDir
32+
var tempDir: Path? = null
33+
1834
@Test
1935
fun testFromEnvironmentFavorsExplicitConfig() = runTest {
2036
val explicitRegion = "explicit-region"
@@ -41,6 +57,53 @@ class AbstractAwsSdkClientFactoryTest {
4157
assertIs<StandardRetryStrategy>(client.config.retryStrategy)
4258
}
4359
}
60+
61+
@Test
62+
fun testFromEnvironmentResolvesAppId() = runTest(
63+
timeout = 20.seconds,
64+
) {
65+
val credentialsFile = tempDir!!.resolve("credentials")
66+
val configFile = tempDir!!.resolve("config")
67+
68+
configFile.writeText("[profile foo]\nsdk_ua_app_id = profile-app-id")
69+
70+
val testPlatform = mockPlatform(
71+
pathSegment = PlatformProvider.System.filePathSeparator,
72+
awsProfileEnv = "foo",
73+
homeEnv = "/home/user",
74+
awsConfigFileEnv = configFile.absolutePathString(),
75+
awsSharedCredentialsFileEnv = credentialsFile.absolutePathString(),
76+
os = PlatformProvider.System.osInfo(),
77+
)
78+
79+
val sharedConfig = asyncLazy { loadAwsSharedConfig(testPlatform) }
80+
val profile = asyncLazy { sharedConfig.get().activeProfile }
81+
82+
assertEquals("profile-app-id", resolveUserAgentAppId(testPlatform, profile))
83+
84+
configFile.deleteIfExists()
85+
credentialsFile.deleteIfExists()
86+
87+
withEnvironment(
88+
mapOf(
89+
AwsSdkSetting.AwsAppId.envVar to "env-app-id",
90+
),
91+
) {
92+
assertEquals("env-app-id", TestClient.fromEnvironment().config.applicationId)
93+
94+
withSystemProperties(
95+
mapOf(
96+
AwsSdkSetting.AwsAppId.sysProp to "system-properties-app-id",
97+
),
98+
) {
99+
assertEquals("system-properties-app-id", TestClient.fromEnvironment().config.applicationId)
100+
assertEquals(
101+
"explicit-app-id",
102+
TestClient.fromEnvironment { applicationId = "explicit-app-id" }.config.applicationId,
103+
)
104+
}
105+
}
106+
}
44107
}
45108

46109
private interface TestClient : SdkClient {
@@ -59,9 +122,10 @@ private interface TestClient : SdkClient {
59122
class Config private constructor(builder: Builder) : SdkClientConfig, AwsSdkClientConfig, RetryStrategyClientConfig by builder.buildRetryStrategyClientConfig() {
60123
override val clientName: String = builder.clientName
61124
override val logMode: LogMode = builder.logMode ?: LogMode.Default
62-
override val region: String = builder.region ?: error("region is required")
125+
override val region: String? = builder.region
63126
override var useFips: Boolean = builder.useFips ?: false
64127
override var useDualStack: Boolean = builder.useDualStack ?: false
128+
override val applicationId: String? = builder.applicationId
65129

66130
// new: inherits builder equivalents for Config base classes
67131
class Builder : AwsSdkClientConfig.Builder, SdkClientConfig.Builder<Config>, RetryStrategyClientConfig.Builder by RetryStrategyClientConfigImpl.BuilderImpl() {
@@ -70,6 +134,7 @@ private interface TestClient : SdkClient {
70134
override var region: String? = null
71135
override var useFips: Boolean? = null
72136
override var useDualStack: Boolean? = null
137+
override var applicationId: String? = null
73138
override fun build(): Config = Config(this)
74139
}
75140
}

aws-runtime/aws-config/jvm/test/aws/sdk/kotlin/runtime/config/profile/AWSConfigLoaderFilesystemTest.kt

Lines changed: 1 addition & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,11 @@
55

66
package aws.sdk.kotlin.runtime.config.profile
77

8-
import aws.smithy.kotlin.runtime.util.OperatingSystem
8+
import aws.sdk.kotlin.runtime.config.utils.mockPlatform
99
import aws.smithy.kotlin.runtime.util.PlatformProvider
10-
import io.mockk.coEvery
11-
import io.mockk.every
12-
import io.mockk.mockk
13-
import io.mockk.slot
1410
import kotlinx.coroutines.test.runTest
1511
import org.junit.jupiter.api.Test
1612
import org.junit.jupiter.api.io.TempDir
17-
import java.io.File
1813
import java.nio.file.Path
1914
import kotlin.io.path.absolutePathString
2015
import kotlin.io.path.deleteIfExists
@@ -81,51 +76,4 @@ class AWSConfigLoaderFilesystemTest {
8176
configFile.deleteIfExists()
8277
credentialsFile.deleteIfExists()
8378
}
84-
85-
private fun mockPlatform(
86-
pathSegment: String,
87-
awsProfileEnv: String? = null,
88-
awsConfigFileEnv: String? = null,
89-
homeEnv: String? = null,
90-
awsSharedCredentialsFileEnv: String? = null,
91-
homeProp: String? = null,
92-
os: OperatingSystem,
93-
): PlatformProvider {
94-
val testPlatform = mockk<PlatformProvider>()
95-
val envKeyParam = slot<String>()
96-
val propKeyParam = slot<String>()
97-
val filePath = slot<String>()
98-
99-
every { testPlatform.filePathSeparator } returns pathSegment
100-
every { testPlatform.getenv(capture(envKeyParam)) } answers {
101-
when (envKeyParam.captured) {
102-
"AWS_PROFILE" -> awsProfileEnv
103-
"AWS_CONFIG_FILE" -> awsConfigFileEnv
104-
"HOME" -> homeEnv
105-
"AWS_SHARED_CREDENTIALS_FILE" -> awsSharedCredentialsFileEnv
106-
else -> error(envKeyParam.captured)
107-
}
108-
}
109-
every { testPlatform.getProperty(capture(propKeyParam)) } answers {
110-
if (propKeyParam.captured == "user.home") homeProp else null
111-
}
112-
every { testPlatform.osInfo() } returns os
113-
coEvery {
114-
testPlatform.readFileOrNull(capture(filePath))
115-
} answers {
116-
if (awsConfigFileEnv != null) {
117-
val file = if (filePath.captured.endsWith("config")) {
118-
File(awsConfigFileEnv)
119-
} else {
120-
File(awsSharedCredentialsFileEnv)
121-
}
122-
123-
if (file.exists()) file.readBytes() else null
124-
} else {
125-
null
126-
}
127-
}
128-
129-
return testPlatform
130-
}
13179
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package aws.sdk.kotlin.runtime.config.utils
7+
8+
import aws.smithy.kotlin.runtime.util.OperatingSystem
9+
import aws.smithy.kotlin.runtime.util.PlatformProvider
10+
import io.mockk.coEvery
11+
import io.mockk.every
12+
import io.mockk.mockk
13+
import io.mockk.slot
14+
import java.io.File
15+
16+
internal fun mockPlatform(
17+
pathSegment: String,
18+
awsProfileEnv: String? = null,
19+
awsConfigFileEnv: String? = null,
20+
homeEnv: String? = null,
21+
awsSharedCredentialsFileEnv: String? = null,
22+
awsSdkUserAgentAppIdEnv: String? = null,
23+
homeProp: String? = null,
24+
os: OperatingSystem,
25+
): PlatformProvider {
26+
val testPlatform = mockk<PlatformProvider>()
27+
val envKeyParam = slot<String>()
28+
val propKeyParam = slot<String>()
29+
val filePath = slot<String>()
30+
31+
every { testPlatform.filePathSeparator } returns pathSegment
32+
every { testPlatform.getenv(capture(envKeyParam)) } answers {
33+
when (envKeyParam.captured) {
34+
"AWS_PROFILE" -> awsProfileEnv
35+
"AWS_CONFIG_FILE" -> awsConfigFileEnv
36+
"HOME" -> homeEnv
37+
"AWS_SHARED_CREDENTIALS_FILE" -> awsSharedCredentialsFileEnv
38+
"AWS_SDK_UA_APP_ID" -> awsSdkUserAgentAppIdEnv
39+
else -> error(envKeyParam.captured)
40+
}
41+
}
42+
every { testPlatform.getProperty(capture(propKeyParam)) } answers {
43+
if (propKeyParam.captured == "user.home") homeProp else null
44+
}
45+
every { testPlatform.osInfo() } returns os
46+
coEvery {
47+
testPlatform.readFileOrNull(capture(filePath))
48+
} answers {
49+
if (awsConfigFileEnv != null) {
50+
val file = if (filePath.captured.endsWith("config")) {
51+
File(awsConfigFileEnv)
52+
} else {
53+
File(awsSharedCredentialsFileEnv)
54+
}
55+
56+
if (file.exists()) file.readBytes() else null
57+
} else {
58+
null
59+
}
60+
}
61+
62+
return testPlatform
63+
}

aws-runtime/aws-core/api/aws-core.api

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,18 @@ public final class aws/sdk/kotlin/runtime/client/AwsClientOption {
3838
}
3939

4040
public abstract interface class aws/sdk/kotlin/runtime/client/AwsSdkClientConfig : aws/smithy/kotlin/runtime/client/SdkClientConfig {
41+
public abstract fun getApplicationId ()Ljava/lang/String;
4142
public abstract fun getRegion ()Ljava/lang/String;
4243
public abstract fun getUseDualStack ()Z
4344
public abstract fun getUseFips ()Z
4445
}
4546

4647
public abstract interface class aws/sdk/kotlin/runtime/client/AwsSdkClientConfig$Builder {
48+
public abstract fun getApplicationId ()Ljava/lang/String;
4749
public abstract fun getRegion ()Ljava/lang/String;
4850
public abstract fun getUseDualStack ()Ljava/lang/Boolean;
4951
public abstract fun getUseFips ()Ljava/lang/Boolean;
52+
public abstract fun setApplicationId (Ljava/lang/String;)V
5053
public abstract fun setRegion (Ljava/lang/String;)V
5154
public abstract fun setUseDualStack (Ljava/lang/Boolean;)V
5255
public abstract fun setUseFips (Ljava/lang/Boolean;)V

0 commit comments

Comments
 (0)