Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add more user agent app id sources #1071

Merged
merged 9 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Comment on lines -62 to +125
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: what changed here, is region no longer required?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check wasn't required to begin with. I removed it because it was interfering with the applicationId tests. I would've had to add a region to the TestClient from the tests even if it wasn't necessary

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
Loading