Skip to content

Commit

Permalink
fix: overhaul endpoint discovery to favor endpointUrl when provided, …
Browse files Browse the repository at this point in the history
…make endpoint discoverers interfaces so custom implementations can be provided, and to respect environmental config (#1506)
  • Loading branch information
ianbotsf authored Jan 17, 2025
1 parent 7e033d9 commit bd6fff1
Show file tree
Hide file tree
Showing 14 changed files with 236 additions and 4 deletions.
9 changes: 9 additions & 0 deletions .changes/49af01b8-6fed-4add-ace0-9f027e83425a.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"id": "49af01b8-6fed-4add-ace0-9f027e83425a",
"type": "feature",
"description": "⚠️ **IMPORTANT**: Refactor endpoint discoverer classes into interfaces so custom implementations may be provided",
"issues": [
"awslabs/aws-sdk-kotlin#1413"
],
"requiresMinorVersionBump": true
}
9 changes: 9 additions & 0 deletions .changes/929f0e2a-3af9-4f73-9f1b-b4e97f91f0db.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"id": "929f0e2a-3af9-4f73-9f1b-b4e97f91f0db",
"type": "feature",
"description": "⚠️ **IMPORTANT**: Add support for enabling/disabling endpoint discovery via [standard cross-SDK config mechanisms](https://docs.aws.amazon.com/sdkref/latest/guide/feature-endpoint-discovery.html)",
"issues": [
"awslabs/aws-sdk-kotlin#1413"
],
"requiresMinorVersionBump": true
}
8 changes: 8 additions & 0 deletions .changes/e6515649-dab5-4be9-b4b4-b289369960d5.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"id": "e6515649-dab5-4be9-b4b4-b289369960d5",
"type": "bugfix",
"description": "Favor `endpointUrl` instead of endpoint discovery if both are provided",
"issues": [
"awslabs/aws-sdk-kotlin#1413"
]
}
4 changes: 4 additions & 0 deletions aws-runtime/aws-config/api/aws-config.api
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ public final class aws/sdk/kotlin/runtime/config/AwsSdkSetting {
public final fun getAwsEc2MetadataDisabled ()Laws/smithy/kotlin/runtime/config/EnvironmentSetting;
public final fun getAwsEc2MetadataServiceEndpoint ()Laws/smithy/kotlin/runtime/config/EnvironmentSetting;
public final fun getAwsEc2MetadataServiceEndpointMode ()Laws/smithy/kotlin/runtime/config/EnvironmentSetting;
public final fun getAwsEndpointDiscoveryEnabled ()Laws/smithy/kotlin/runtime/config/EnvironmentSetting;
public final fun getAwsEndpointUrl ()Laws/smithy/kotlin/runtime/config/EnvironmentSetting;
public final fun getAwsExecutionEnv ()Laws/smithy/kotlin/runtime/config/EnvironmentSetting;
public final fun getAwsIgnoreEndpointUrls ()Laws/smithy/kotlin/runtime/config/EnvironmentSetting;
Expand Down Expand Up @@ -311,6 +312,8 @@ public final class aws/sdk/kotlin/runtime/config/endpoints/ResolversKt {
public static final fun resolveAccountId (Laws/sdk/kotlin/runtime/config/endpoints/AccountIdEndpointMode;Laws/smithy/kotlin/runtime/collections/Attributes;)Ljava/lang/String;
public static final fun resolveAccountIdEndpointMode (Laws/smithy/kotlin/runtime/util/PlatformProvider;Laws/smithy/kotlin/runtime/util/LazyAsyncValue;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun resolveAccountIdEndpointMode$default (Laws/smithy/kotlin/runtime/util/PlatformProvider;Laws/smithy/kotlin/runtime/util/LazyAsyncValue;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public static final fun resolveEndpointDiscoveryEnabled (Laws/smithy/kotlin/runtime/util/PlatformProvider;Laws/smithy/kotlin/runtime/util/LazyAsyncValue;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun resolveEndpointDiscoveryEnabled$default (Laws/smithy/kotlin/runtime/util/PlatformProvider;Laws/smithy/kotlin/runtime/util/LazyAsyncValue;ZLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public static final fun resolveEndpointUrl (Laws/smithy/kotlin/runtime/util/LazyAsyncValue;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Laws/smithy/kotlin/runtime/util/PlatformProvider;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun resolveEndpointUrl$default (Laws/smithy/kotlin/runtime/util/LazyAsyncValue;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Laws/smithy/kotlin/runtime/util/PlatformProvider;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public static final fun resolveUseDualStack (Laws/smithy/kotlin/runtime/util/PlatformProvider;Laws/smithy/kotlin/runtime/util/LazyAsyncValue;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
Expand Down Expand Up @@ -490,6 +493,7 @@ public final class aws/sdk/kotlin/runtime/config/profile/AwsProfileKt {
public static synthetic fun getBooleanOrNull$default (Laws/sdk/kotlin/runtime/config/profile/ConfigSection;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Ljava/lang/Boolean;
public static final fun getCredentialProcess (Laws/sdk/kotlin/runtime/config/profile/ConfigSection;)Ljava/lang/String;
public static final fun getDisableRequestCompression (Laws/sdk/kotlin/runtime/config/profile/ConfigSection;)Ljava/lang/Boolean;
public static final fun getEndpointDiscoveryEnabled (Laws/sdk/kotlin/runtime/config/profile/ConfigSection;)Ljava/lang/Boolean;
public static final fun getEndpointUrl (Laws/sdk/kotlin/runtime/config/profile/ConfigSection;)Laws/smithy/kotlin/runtime/net/url/Url;
public static final fun getIgnoreEndpointUrls (Laws/sdk/kotlin/runtime/config/profile/ConfigSection;)Ljava/lang/Boolean;
public static final fun getIntOrNull (Laws/sdk/kotlin/runtime/config/profile/ConfigSection;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Integer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,15 @@ public object AwsSdkSetting {
public val AwsSigV4aSigningRegionSet: EnvironmentSetting<String> =
strEnvSetting("aws.sigV4aSigningRegionSet", "AWS_SIGV4A_SIGNING_REGION_SET")

/**
* A flag indicating whether endpoint discovery is enabled for AWS services that support it. The implicit default
* value for this setting is:
* * `true` for services which _require_ EP discovery (e.g., Timestream)
* * `false` for services which _allow but do not require_ EP discovery (e.g., DynamoDB)
*/
public val AwsEndpointDiscoveryEnabled: EnvironmentSetting<Boolean> =
boolEnvSetting("aws.endpointDiscoveryEnabled", "AWS_ENABLE_ENDPOINT_DISCOVERY")

/**
* Configures request checksum calculation
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,16 @@ public fun resolveAccountId(endpointMode: AccountIdEndpointMode, attributes: Att
AccountIdEndpointMode.DISABLED -> null
AccountIdEndpointMode.REQUIRED -> attributes.getOrNull(AwsClientOption.AccountId) ?: throw ConfigurationException("AccountIdEndpointMode is set to required but no AWS account ID found")
}

/**
* Resolve the endpoint discovery mode
*/
@InternalSdkApi
public suspend fun resolveEndpointDiscoveryEnabled(
provider: PlatformProvider = PlatformProvider.System,
profile: LazyAsyncValue<AwsProfile> = asyncLazy { loadAwsSharedConfig(provider).activeProfile },
serviceRequiresEpDiscovery: Boolean,
): Boolean =
AwsSdkSetting.AwsEndpointDiscoveryEnabled.resolve(provider)
?: profile.get().endpointDiscoveryEnabled
?: serviceRequiresEpDiscovery
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,14 @@ public val AwsProfile.requestMinCompressionSizeBytes: Long?
public val AwsProfile.sigV4aSigningRegionSet: String?
get() = getOrNull("sigv4a_signing_region_set")

/**
* A flag indicating whether endpoint discovery should be enabled for a service that supports it. This setting has no
* effect for services which _do not_ support endpoint discovery.
*/
@InternalSdkApi
public val AwsProfile.endpointDiscoveryEnabled: Boolean?
get() = getBooleanOrNull("endpoint_discovery_enabled")

/**
* Configures request checksum calculation
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.sdk.kotlin.runtime.config.endpoints

import aws.sdk.kotlin.runtime.config.profile.*
import aws.sdk.kotlin.runtime.config.profile.FileType
import aws.sdk.kotlin.runtime.config.profile.parse
import aws.sdk.kotlin.runtime.config.profile.toSharedConfig
import aws.smithy.kotlin.runtime.telemetry.logging.Logger
import aws.smithy.kotlin.runtime.util.TestPlatformProvider
import aws.smithy.kotlin.runtime.util.asyncLazy
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals

class ResolveEndpointDiscoveryTest {
@Test
fun testPrecedenceSysProps() = assertEpDiscovery(
sysProps = mapOf("aws.endpointDiscoveryEnabled" to "true"),
env = mapOf("AWS_ENABLE_ENDPOINT_DISCOVERY" to "false"),
config = """
[${Literals.DEFAULT_PROFILE}]
endpoint_discovery_enabled = false
""".trimIndent(),
serviceRequiresEpDiscovery = false,
expected = true,
)

@Test
fun testPrecedenceEnvVars() = assertEpDiscovery(
env = mapOf("AWS_ENABLE_ENDPOINT_DISCOVERY" to "true"),
config = """
[${Literals.DEFAULT_PROFILE}]
endpoint_discovery_enabled = false
""".trimIndent(),
serviceRequiresEpDiscovery = false,
expected = true,
)

@Test
fun testPrecedenceConfig() = assertEpDiscovery(
config = """
[${Literals.DEFAULT_PROFILE}]
endpoint_discovery_enabled = true
""".trimIndent(),
serviceRequiresEpDiscovery = false,
expected = true,
)

@Test
fun testPrecedenceDefault() = assertEpDiscovery(
serviceRequiresEpDiscovery = true,
expected = true,
)
}

fun assertEpDiscovery(
sysProps: Map<String, String> = mapOf(),
env: Map<String, String> = mapOf(),
config: String = "",
serviceRequiresEpDiscovery: Boolean,
expected: Boolean,
) = runTest {
val provider = TestPlatformProvider(env, sysProps)
val source = AwsConfigurationSource(Literals.DEFAULT_PROFILE, "", "")

val profile = asyncLazy {
parse(Logger.None, FileType.CONFIGURATION, config)
.toSharedConfig(source)
.activeProfile
}

val actual = resolveEndpointDiscoveryEnabled(provider, profile, serviceRequiresEpDiscovery)
assertEquals(expected, actual)
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ object AwsRuntimeTypes {

object Endpoints : RuntimeTypePackage(AwsKotlinDependency.AWS_CONFIG, "config.endpoints") {
val AccountIdEndpointMode = symbol("AccountIdEndpointMode")
val resolveEndpointDiscoveryEnabled = symbol("resolveEndpointDiscoveryEnabled")
val resolveEndpointUrl = symbol("resolveEndpointUrl")
val resolveAccountId = symbol("resolveAccountId")
val resolveAccountIdEndpointMode = symbol("resolveAccountIdEndpointMode")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ class PresignerGenerator : KotlinIntegration {
* signing.
*/
object UnsignedRequestCustomizationSection : SectionId {
val CodegenContext: SectionKey<CodegenContext> = SectionKey("CodegenContext")
val OperationId: SectionKey<String> = SectionKey("OperationId")
val HttpBindingResolver: SectionKey<HttpBindingResolver> = SectionKey("HttpBindingResolver")
val DefaultTimestampFormat: SectionKey<TimestampFormatTrait.Format> = SectionKey("DefaultTimestampFormat")
Expand Down Expand Up @@ -126,7 +125,6 @@ class PresignerGenerator : KotlinIntegration {

val contextMap: Map<SectionKey<*>, Any> = mapOf(
UnsignedRequestCustomizationSection.OperationId to op.id.toString(),
UnsignedRequestCustomizationSection.CodegenContext to ctx,
UnsignedRequestCustomizationSection.HttpBindingResolver to httpBindingResolver,
UnsignedRequestCustomizationSection.DefaultTimestampFormat to defaultTimestampFormat,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class ReplaceServiceExceptionBase : KotlinIntegration {
get() = listOf(SectionWriterBinding(ExceptionBaseClassGenerator.ExceptionBaseClassSection, exceptionSectionWriter))

private val exceptionSectionWriter = SectionWriter { writer, _ ->
val ctx = writer.getContextValue(ExceptionBaseClassGenerator.ExceptionBaseClassSection.CodegenContext)
val ctx = writer.getContextValue(CodegenContext.Key)
AwsServiceExceptionBaseClassGenerator().render(ctx, writer)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class PollyPresigner : KotlinIntegration {
}

private val customizeUnsignedRequest = SectionWriter { writer, _ ->
val ctx = writer.getContextValue(PresignerGenerator.UnsignedRequestCustomizationSection.CodegenContext)
val ctx = writer.getContextValue(CodegenContext.Key)
val operation = ctx.model.expectShape<OperationShape>(writer.getContextValue(PresignerGenerator.UnsignedRequestCustomizationSection.OperationId))
val resolver = writer.getContextValue(PresignerGenerator.UnsignedRequestCustomizationSection.HttpBindingResolver)
val defaultTimestampFormat = writer.getContextValue(PresignerGenerator.UnsignedRequestCustomizationSection.DefaultTimestampFormat)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.sdk.kotlin.codegen.endpoints

import aws.sdk.kotlin.codegen.AwsRuntimeTypes
import aws.sdk.kotlin.codegen.ServiceClientCompanionObjectWriter
import software.amazon.smithy.kotlin.codegen.KotlinSettings
import software.amazon.smithy.kotlin.codegen.core.CodegenContext
import software.amazon.smithy.kotlin.codegen.core.KotlinDelegator
import software.amazon.smithy.kotlin.codegen.core.getContextValue
import software.amazon.smithy.kotlin.codegen.integration.AppendingSectionWriter
import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration
import software.amazon.smithy.kotlin.codegen.integration.SectionWriterBinding
import software.amazon.smithy.kotlin.codegen.model.asNullable
import software.amazon.smithy.kotlin.codegen.rendering.endpoints.discovery.DefaultEndpointDiscovererGenerator
import software.amazon.smithy.kotlin.codegen.rendering.endpoints.discovery.EndpointDiscovererInterfaceGenerator
import software.amazon.smithy.kotlin.codegen.rendering.endpoints.discovery.EndpointDiscoveryIntegration
import software.amazon.smithy.kotlin.codegen.rendering.util.ConfigProperty
import software.amazon.smithy.kotlin.codegen.rendering.util.ConfigPropertyType
import software.amazon.smithy.model.Model

class AwsEndpointDiscoveryIntegration : KotlinIntegration {
override val order: Byte = (EndpointDiscoveryIntegration.ORDER + 1).toByte() // after EndpointDiscoveryIntegration

override fun additionalServiceConfigProps(ctx: CodegenContext): List<ConfigProperty> {
val endpointDiscoveryOptional = EndpointDiscoveryIntegration.isOptionalFor(ctx)
val interfaceSymbol = EndpointDiscovererInterfaceGenerator.symbolFor(ctx.settings)
return listOf(
ConfigProperty {
name = EndpointDiscoveryIntegration.CLIENT_CONFIG_NAME
symbol = interfaceSymbol.asNullable()

if (endpointDiscoveryOptional) {
documentation = """
The endpoint discoverer for this client, if applicable. By default, no endpoint discovery is
provided. To use endpoint discovery, set this to a valid [${interfaceSymbol.name}] instance.
""".trimIndent()
propertyType = ConfigPropertyType.SymbolDefault
} else {
val defaultImplSymbol = DefaultEndpointDiscovererGenerator.symbolFor(ctx.settings)

documentation = """
The endpoint discoverer for this client, [${defaultImplSymbol.name}] by default.
""".trimIndent()
propertyType = ConfigPropertyType.Custom(
render = { prop, writer ->
writer.write(
"#1L val #2L: #3T = builder.#2L ?: #4T()",
ctx.settings.api.visibility,
prop.propertyName,
prop.symbol,
defaultImplSymbol,
)
},
)
}
},
)
}

override fun enabledForService(model: Model, settings: KotlinSettings): Boolean =
EndpointDiscoveryIntegration.isEnabledFor(model, settings)

private val resolveEndpointDiscoverer = AppendingSectionWriter { writer ->
val ctx = writer.getContextValue(CodegenContext.Key)
val endpointDiscoveryOptional = EndpointDiscoveryIntegration.isOptionalFor(ctx)

writer.write(
"val epDiscoveryEnabled = #T(profile = activeProfile, serviceRequiresEpDiscovery = #L)",
AwsRuntimeTypes.Config.Endpoints.resolveEndpointDiscoveryEnabled,
!endpointDiscoveryOptional,
)

writer.write(
"builder.config.#1L = builder.config.#1L ?: if (epDiscoveryEnabled) #2T() else null",
EndpointDiscoveryIntegration.CLIENT_CONFIG_NAME,
DefaultEndpointDiscovererGenerator.symbolFor(ctx.settings),
)
}

override val sectionWriters = listOf(
SectionWriterBinding(ServiceClientCompanionObjectWriter.FinalizeEnvironmentalConfig, resolveEndpointDiscoverer),
)

override fun writeAdditionalFiles(ctx: CodegenContext, delegator: KotlinDelegator) {
// EndpointDiscoveryIntegration already renders the default endpoint discoverer for services that _require_ EP
// discovery. So we only need to render it for services which _do not require_ EP discovery in order to support
// enabling discovery via environmental config.
if (EndpointDiscoveryIntegration.isOptionalFor(ctx)) {
DefaultEndpointDiscovererGenerator(ctx, delegator).render()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ aws.sdk.kotlin.codegen.customization.DefaultMiddleware
aws.sdk.kotlin.codegen.customization.AccountIdEndpointBuiltinCustomization
aws.sdk.kotlin.codegen.customization.PresignableModelIntegration
aws.sdk.kotlin.codegen.customization.BackfillOptionalAuth
aws.sdk.kotlin.codegen.endpoints.AwsEndpointDiscoveryIntegration
aws.sdk.kotlin.codegen.endpoints.BindAwsEndpointBuiltins
aws.sdk.kotlin.codegen.endpoints.AddAwsEndpointFunctions
aws.sdk.kotlin.codegen.PresignerGenerator
Expand Down

0 comments on commit bd6fff1

Please sign in to comment.