Skip to content

Commit

Permalink
feat: add more user agent app id sources (#1071)
Browse files Browse the repository at this point in the history
  • Loading branch information
0marperez authored Oct 11, 2023
1 parent 3d1d564 commit 24851dc
Show file tree
Hide file tree
Showing 16 changed files with 276 additions and 63 deletions.
8 changes: 8 additions & 0 deletions .changes/2e8e4fee-c462-45d2-b421-46520d06eb60.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
3 changes: 3 additions & 0 deletions aws-runtime/aws-config/api/aws-config.api
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -45,6 +47,11 @@ public object AwsSdkSetting {
*/
public val AwsRegion: EnvironmentSetting<String> = strEnvSetting("aws.region", "AWS_REGION")

/**
* Configure the user agent app ID
*/
public val AwsAppId: EnvironmentSetting<String> = strEnvSetting(AWS_APP_ID_PROP, AWS_APP_ID_ENV)

/**
* Configure the default path to the shared config file.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AwsProfile>): String? =
AwsSdkSetting.AwsAppId.resolve(platform) ?: profile.get().sdkUserAgentAppId
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -41,6 +57,53 @@ class AbstractAwsSdkClientFactoryTest {
assertIs<StandardRetryStrategy>(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 {
Expand All @@ -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<Config>, RetryStrategyClientConfig.Builder by RetryStrategyClientConfigImpl.BuilderImpl() {
Expand All @@ -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)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<PlatformProvider>()
val envKeyParam = slot<String>()
val propKeyParam = slot<String>()
val filePath = slot<String>()

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
}
}
Original file line number Diff line number Diff line change
@@ -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<PlatformProvider>()
val envKeyParam = slot<String>()
val propKeyParam = slot<String>()
val filePath = slot<String>()

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
}
3 changes: 3 additions & 0 deletions aws-runtime/aws-core/api/aws-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 24851dc

Please sign in to comment.