Skip to content

Commit b2ada8d

Browse files
authored
feat: emit bearer business metric (#1658)
* refactor tests and enable sourcing token from sysprops * refactor * consistent naming * naming * align with new constructor * add kdoc * rename transform file * refactor transformers * bump smithy version
1 parent c4d03d9 commit b2ada8d

File tree

6 files changed

+153
-93
lines changed

6 files changed

+153
-93
lines changed

codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/SdkIdTransform.kt

Lines changed: 0 additions & 61 deletions
This file was deleted.

codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/ServiceClientCompanionObjectWriter.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,15 @@ internal data class EndpointUrlConfigNames(
6464

6565
internal fun String.toEndpointUrlConfigNames(): EndpointUrlConfigNames = EndpointUrlConfigNames(
6666
withTransform(JvmSystemPropertySuffix),
67-
withTransform(SdkIdTransform.UpperSnakeCase),
68-
withTransform(SdkIdTransform.LowerSnakeCase),
67+
withTransform(SdkIdTransformers.UpperSnakeCase),
68+
withTransform(SdkIdTransformers.LowerSnakeCase),
6969
)
7070

7171
// JVM system property names follow the pattern "aws.endpointUrl${BaseClientName}"
7272
// where BaseClientName is the PascalCased sdk ID with any forbidden suffixes dropped - this is the same as what we use
7373
// for our client names
7474
// e.g. sdkId "Elasticsearch Service" -> client name "ElasticsearchClient", prop "aws.endpointUrlElasticsearch"
75-
private object JvmSystemPropertySuffix : SdkIdTransformer {
75+
private object JvmSystemPropertySuffix : StringTransformer {
7676
override fun transform(id: String): String =
7777
id.toPascalCase().removeSuffix("Service")
7878
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.sdk.kotlin.codegen
6+
7+
import software.amazon.smithy.kotlin.codegen.utils.toPascalCase
8+
9+
private val whitespaceRegex = Regex("\\s")
10+
11+
private val spaceOrDashRegex = Regex("\\s|-")
12+
13+
/**
14+
* Base interface for string transformers.
15+
*/
16+
fun interface StringTransformer {
17+
fun transform(input: String): String
18+
}
19+
20+
/**
21+
* Implements all standardized sdkId transforms.
22+
*/
23+
object SdkIdTransformers {
24+
// Replace all whitespace from the sdkId with underscores and lowercase all letters.
25+
val LowerSnakeCase = StringTransformer { it.replaceWhitespace("_").lowercase() }
26+
27+
// None. Directly use the sdkId.
28+
val Identity = StringTransformer { it }
29+
30+
// Remove all whitespace from the sdkId.
31+
val NoWhitespace = StringTransformer { it.replaceWhitespace("") }
32+
33+
// Replace all whitespace from the sdkId with dashes and lowercase all letters.
34+
val LowerKebabCase = StringTransformer { it.replaceWhitespace("-").lowercase() }
35+
36+
// Replace all whitespace from the sdkId with underscores and capitalize all letters.
37+
val UpperSnakeCase = StringTransformer { it.replaceWhitespace("_").uppercase() }
38+
}
39+
40+
/**
41+
* Implements all standardized SigV4 service signing name transforms.
42+
*/
43+
object SigV4NameTransformers {
44+
// Replace all dashes from the SigV4 service signing name with underscores and capitalize all letters.
45+
val UpperSnakeCase = StringTransformer { it.replaceSpaceOrDash("_").uppercase() }
46+
47+
// Remove dashes and convert SigV4 service signing name to PascalCase
48+
val PascalCase = StringTransformer { it.toPascalCase() }
49+
}
50+
51+
/**
52+
* Applies the given transformer to the string.
53+
*/
54+
fun <T : StringTransformer> String.withTransform(transformer: T): String = transformer.transform(this)
55+
56+
private fun String.replaceWhitespace(replacement: String) = replace(whitespaceRegex, replacement)
57+
58+
private fun String.replaceSpaceOrDash(replacement: String) = replace(spaceOrDashRegex, replacement)

codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/customization/EnvironmentBearerTokenCustomization.kt

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
*/
55
package aws.sdk.kotlin.codegen.customization
66

7-
import aws.sdk.kotlin.codegen.SdkIdTransform
87
import aws.sdk.kotlin.codegen.ServiceClientCompanionObjectWriter
8+
import aws.sdk.kotlin.codegen.SigV4NameTransformers
99
import aws.sdk.kotlin.codegen.withTransform
1010
import software.amazon.smithy.kotlin.codegen.KotlinSettings
1111
import software.amazon.smithy.kotlin.codegen.core.*
@@ -22,15 +22,15 @@ import software.amazon.smithy.model.shapes.ServiceShape
2222
import software.amazon.smithy.model.traits.HttpBearerAuthTrait
2323

2424
/**
25-
* Customization that enables sourcing Bearer tokens from an environment variable
25+
* Customization that enables sourcing Bearer tokens from JVM system properties and system environment variables
2626
*
27-
* When a service-specific environment variable for bearer tokens is present (e.g., AWS_BEARER_TOKEN_BEDROCK),
28-
* this customization configures the auth scheme resolver to prefer the smithy.api#httpBearerAuth scheme
29-
* over other authentication methods. Additionally, it configures a token provider that extracts the bearer token
30-
* from the target environment variable.
27+
* When a service-specific JVM system property (e.g., aws.bearerTokenBedrock) or system environment variable
28+
* for bearer tokens is present (e.g., AWS_BEARER_TOKEN_BEDROCK), this customization configures the
29+
* auth scheme resolver to prefer the smithy.api#httpBearerAuth scheme over other authentication methods.
30+
* Additionally, it configures a token provider that extracts the bearer token from these sources.
3131
*/
3232
class EnvironmentBearerTokenCustomization : KotlinIntegration {
33-
// Currently only services with sigv4 service name 'bedrock' need this customization
33+
// Currently only services with sigV4 service name 'bedrock' need this customization
3434
private val supportedSigningServiceNames = setOf("bedrock")
3535

3636
override fun enabledForService(model: Model, settings: KotlinSettings): Boolean {
@@ -68,9 +68,11 @@ class EnvironmentBearerTokenCustomization : KotlinIntegration {
6868
) {
6969
val serviceSymbol = ctx.symbolProvider.toSymbol(serviceShape)
7070
val signingServiceName = AwsSignatureVersion4.signingServiceName(serviceShape)
71-
// Transform signing name to environment variable name
72-
val envVarSuffix = signingServiceName.withTransform(SdkIdTransform.UpperSnakeCase)
73-
val envVarName = "AWS_BEARER_TOKEN_$envVarSuffix"
71+
// Transform signing service name to environment variable key suffix
72+
val envSuffix = signingServiceName.withTransform(SigV4NameTransformers.UpperSnakeCase)
73+
val sysPropSuffix = signingServiceName.withTransform(SigV4NameTransformers.PascalCase)
74+
val envKey = "AWS_BEARER_TOKEN_$envSuffix"
75+
val sysPropKey = "aws.bearerToken$sysPropSuffix"
7476
val authSchemeId = RuntimeTypes.Auth.Identity.AuthSchemeId
7577

7678
writer.withBlock(
@@ -79,9 +81,8 @@ class EnvironmentBearerTokenCustomization : KotlinIntegration {
7981
serviceSymbol,
8082
RuntimeTypes.Core.Utils.PlatformProvider,
8183
) {
82-
// The customization do nothing if environment variable is not set
83-
write("if (provider.getenv(#S) == null) { return }", envVarName)
84-
84+
// The customization does nothing if environment variable and JVM system property are not set
85+
write("if (provider.getProperty(#S) == null && provider.getenv(#S) == null) return", sysPropKey, envKey)
8586
// Configure auth scheme preference if customer hasn't specify one
8687
write("builder.config.authSchemePreference = builder.config.authSchemePreference ?: listOf(#T.HttpBearer)", authSchemeId)
8788

@@ -93,9 +94,10 @@ class EnvironmentBearerTokenCustomization : KotlinIntegration {
9394
write("builder.config.authSchemePreference = listOf(#1T.HttpBearer) + filteredSchemes", authSchemeId)
9495

9596
write(
96-
"builder.config.bearerTokenProvider = builder.config.bearerTokenProvider ?: #T(#S, provider)",
97+
"builder.config.bearerTokenProvider = builder.config.bearerTokenProvider ?: #T(#S, #S, provider)",
9798
RuntimeTypes.Auth.HttpAuth.EnvironmentBearerTokenProvider,
98-
envVarName,
99+
sysPropKey,
100+
envKey,
99101
)
100102
}
101103
}

gradle/libs.versions.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ atomicfu-version = "0.29.0"
1212
binary-compatibility-validator-version = "0.18.0"
1313

1414
# smithy-kotlin codegen and runtime are versioned separately
15-
smithy-kotlin-runtime-version = "1.5.1"
16-
smithy-kotlin-codegen-version = "0.35.1"
15+
smithy-kotlin-runtime-version = "1.5.4"
16+
smithy-kotlin-codegen-version = "0.35.4"
1717

1818
# codegen
1919
smithy-version = "1.60.2"

services/bedrock/common/test/aws/sdk/kotlin/services/bedrock/BedrockEnvironmentBearerTokenTest.kt

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineBase
1515
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineConfig
1616
import aws.smithy.kotlin.runtime.http.request.HttpRequest
1717
import aws.smithy.kotlin.runtime.http.response.HttpResponse
18-
import aws.smithy.kotlin.runtime.io.use
1918
import aws.smithy.kotlin.runtime.operation.ExecutionContext
2019
import aws.smithy.kotlin.runtime.time.Instant
2120
import aws.smithy.kotlin.runtime.util.TestPlatformProvider
2221
import kotlinx.coroutines.test.runTest
22+
import kotlin.test.Test
2323
import kotlin.test.assertEquals
2424
import kotlin.test.assertNotNull
25+
import kotlin.test.assertTrue
2526

2627
class BedrockEnvironmentBearerTokenTest {
2728
private fun mockHttpClient(handler: (HttpRequest) -> HttpResponse): HttpClientEngine {
@@ -39,69 +40,129 @@ class BedrockEnvironmentBearerTokenTest {
3940
env = mapOf("AWS_BEARER_TOKEN_BEDROCK" to "bedrock-token"),
4041
)
4142

43+
@Test
4244
fun testAuthSchemePreferenceConfigured() = runTest {
43-
val builder = BedrockClient.Builder()
4445
val expectedAuthSchemePreference = listOf(AuthSchemeId.HttpBearer)
46+
val builder = BedrockClient.Builder()
47+
4548
finalizeBearerTokenConfig(builder, mockPlatformProvider)
49+
4650
assertEquals(expectedAuthSchemePreference, builder.config.authSchemePreference)
4751
}
4852

53+
@Test
4954
fun testBearerAuthSchemePromotedToFirst() = runTest {
55+
val expectedAuthSchemePreference = listOf(AuthSchemeId.HttpBearer, AuthSchemeId.AwsSigV4)
5056
val builder = BedrockClient.Builder()
57+
5158
builder.config.authSchemePreference = listOf(AuthSchemeId.AwsSigV4)
5259
finalizeBearerTokenConfig(builder, mockPlatformProvider)
53-
val expectedAuthSchemePreference = listOf(AuthSchemeId.HttpBearer, AuthSchemeId.AwsSigV4)
5460
assertEquals(expectedAuthSchemePreference, builder.config.authSchemePreference)
5561

5662
builder.config.authSchemePreference = listOf(AuthSchemeId.AwsSigV4, AuthSchemeId.HttpBearer)
63+
finalizeBearerTokenConfig(builder, mockPlatformProvider)
5764
assertEquals(expectedAuthSchemePreference, builder.config.authSchemePreference)
5865
}
5966

67+
@Test
6068
fun testBearerTokenProviderConfigured() = runTest {
6169
val builder = BedrockClient.Builder()
6270
finalizeBearerTokenConfig(builder, mockPlatformProvider)
71+
6372
assertNotNull(builder.config.bearerTokenProvider)
6473
val token = builder.config.bearerTokenProvider!!.resolve()
6574
assertNotNull(token)
6675
assertEquals("bedrock-token", token.token)
6776
}
6877

78+
@Test
79+
fun testBearerTokenSourcingPrecedence() = runTest {
80+
val builder = BedrockClient.Builder()
81+
82+
finalizeBearerTokenConfig(
83+
builder,
84+
TestPlatformProvider(
85+
env = mapOf("AWS_BEARER_TOKEN_BEDROCK" to "env-bedrock-token"),
86+
props = mapOf("aws.bearerTokenBedrock" to "sys-props-bedrock-token"),
87+
),
88+
)
89+
90+
val token = builder.config.bearerTokenProvider!!.resolve()
91+
assertEquals("sys-props-bedrock-token", token.token)
92+
}
93+
94+
@Test
6995
fun testExplicitProviderTakesPrecedence() = runTest {
7096
val builder = BedrockClient.Builder()
97+
7198
builder.config.bearerTokenProvider = object : BearerTokenProvider {
7299
override suspend fun resolve(attributes: Attributes): BearerToken = object : BearerToken {
73100
override val token: String = "different-bedrock-token"
74101
override val attributes: Attributes = emptyAttributes()
75102
override val expiration: Instant? = null
76103
}
77104
}
105+
78106
finalizeBearerTokenConfig(builder, mockPlatformProvider)
107+
79108
assertNotNull(builder.config.bearerTokenProvider)
80109
val token = builder.config.bearerTokenProvider!!.resolve()
81110
assertNotNull(token)
82111
assertEquals("different-bedrock-token", token.token)
83112
}
84113

114+
@Test
85115
fun testBearerTokenProviderFunctionality() = runTest {
86116
var capturedAuthHeader: String? = null
87117

88-
BedrockClient.fromEnvironment {
89-
region = "us-west-2"
90-
httpClient = mockHttpClient { request ->
91-
// Capture the Authorization header
118+
val builder = BedrockClient.Builder().apply {
119+
config.region = "us-west-2"
120+
config.httpClient = mockHttpClient { request ->
92121
capturedAuthHeader = request.headers["Authorization"]
93122
HttpResponse(
94123
status = HttpStatusCode.OK,
95124
headers = Headers.Empty,
96125
body = HttpBody.Empty,
97126
)
98127
}
99-
}.use { client ->
100-
// Make an api call to capture Authorization header
101-
client.listFoundationModels()
128+
}
129+
130+
finalizeBearerTokenConfig(builder, mockPlatformProvider)
102131

103-
assertNotNull(capturedAuthHeader)
104-
assertEquals("Bearer bedrock-token", capturedAuthHeader)
132+
val testClient = builder.build()
133+
// Make an api call to capture Authorization header
134+
testClient.listFoundationModels()
135+
136+
assertNotNull(capturedAuthHeader)
137+
assertEquals("Bearer bedrock-token", capturedAuthHeader)
138+
}
139+
140+
@Test
141+
fun testBusinessMetricEmitted() = runTest {
142+
var capturedUserAgent: String? = null
143+
144+
val builder = BedrockClient.Builder().apply {
145+
config.region = "us-west-2"
146+
config.httpClient = mockHttpClient { request ->
147+
capturedUserAgent = request.headers["User-Agent"]
148+
HttpResponse(
149+
status = HttpStatusCode.OK,
150+
headers = Headers.Empty,
151+
body = HttpBody.Empty,
152+
)
153+
}
105154
}
155+
156+
finalizeBearerTokenConfig(builder, mockPlatformProvider)
157+
158+
val testClient = builder.build()
159+
// Make an api call to capture User-Agent header
160+
testClient.listFoundationModels()
161+
162+
assertNotNull(capturedUserAgent)
163+
val capturedBusinessMetrics = Regex("m/([^\\s]+)").find(capturedUserAgent!!)?.value
164+
assertNotNull(capturedBusinessMetrics)
165+
// Check User-Agent header contains the business metric
166+
assertTrue(capturedBusinessMetrics.contains("3"))
106167
}
107168
}

0 commit comments

Comments
 (0)