diff --git a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/AwsClientConfigLoader.kt b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/AwsClientConfigLoader.kt new file mode 100644 index 00000000000..2fd33371398 --- /dev/null +++ b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/AwsClientConfigLoader.kt @@ -0,0 +1,60 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package aws.sdk.kotlin.runtime.config + +import aws.sdk.kotlin.runtime.auth.credentials.CredentialsProvider +import aws.sdk.kotlin.runtime.auth.credentials.DefaultChainCredentialsProvider +import aws.sdk.kotlin.runtime.client.AwsClientConfig +import aws.sdk.kotlin.runtime.region.resolveRegion +import aws.smithy.kotlin.runtime.util.Platform +import aws.smithy.kotlin.runtime.util.PlatformProvider + +/** + * Options that control how an [aws.sdk.kotlin.runtime.client.AwsClientConfig] is resolved + */ +public class AwsClientConfigLoadOptions { + // TODO - see Go SDK for an idea of possible knobs + // https://github.com/aws/aws-sdk-go-v2/blob/973e5f5e9477e0690bcb4be3145bb72e956651cc/config/load_options.go#L22 + // Most of these will not be needed if you are using a corresponding environment variable or system property but there should be an explicit + // option to control behavior. + + /** + * The region to make requests to. When set, region will not attempt to be resolved from other sources + */ + public var region: String? = null + + /** + * The [CredentialsProvider] to use when sourcing [aws.sdk.kotlin.runtime.auth.credentials.Credentials]. + */ + public var credentialsProvider: CredentialsProvider? = null + + // FIXME - expose profile name override and thread through region/cred provider chains +} + +/** + * Load the AWS client configuration from the environment. + */ +public suspend fun AwsClientConfig.Companion.fromEnvironment( + block: AwsClientConfigLoadOptions.() -> Unit = {} +): AwsClientConfig = loadAwsClientConfig(Platform, block) + +internal suspend fun loadAwsClientConfig( + platformProvider: PlatformProvider, + block: AwsClientConfigLoadOptions.() -> Unit +): AwsClientConfig { + val opts = AwsClientConfigLoadOptions().apply(block) + + val region = opts.region ?: resolveRegion(platformProvider) + + val credentialsProvider = opts.credentialsProvider ?: DefaultChainCredentialsProvider() + + return ResolvedAwsConfig(region, credentialsProvider) +} + +private data class ResolvedAwsConfig( + override val region: String, + override val credentialsProvider: CredentialsProvider +) : AwsClientConfig diff --git a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/region/DefaultRegionProviderChain.kt b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/region/DefaultRegionProviderChain.kt index 76998b644d6..d490a5810e3 100644 --- a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/region/DefaultRegionProviderChain.kt +++ b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/region/DefaultRegionProviderChain.kt @@ -16,6 +16,6 @@ import aws.smithy.kotlin.runtime.util.PlatformProvider * 3. Check the AWS config files/profile for region information * 4. If running on EC2, check the EC2 metadata service for region */ -public expect class DefaultRegionProviderChain public constructor( +internal expect class DefaultRegionProviderChain constructor( platformProvider: PlatformProvider = Platform ) : RegionProvider, Closeable diff --git a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/region/ImdsRegionProvider.kt b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/region/ImdsRegionProvider.kt index c064a99ff11..8fd7822cbc9 100644 --- a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/region/ImdsRegionProvider.kt +++ b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/region/ImdsRegionProvider.kt @@ -21,7 +21,7 @@ private const val REGION_PATH: String = "/latest/meta-data/placement/region" * @param client the IMDS client to use to resolve region information with * @param platformProvider the [PlatformEnvironProvider] instance */ -public class ImdsRegionProvider( +internal class ImdsRegionProvider( private val client: Lazy = lazy { ImdsClient() }, private val platformProvider: PlatformEnvironProvider = Platform, ) : RegionProvider, Closeable { diff --git a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/region/ResolveRegion.kt b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/region/ResolveRegion.kt index 4025868724e..574cc178888 100644 --- a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/region/ResolveRegion.kt +++ b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/region/ResolveRegion.kt @@ -5,36 +5,14 @@ package aws.sdk.kotlin.runtime.region -import aws.sdk.kotlin.runtime.ClientException -import aws.sdk.kotlin.runtime.InternalSdkApi -import aws.sdk.kotlin.runtime.client.AwsAdvancedClientOption -import aws.sdk.kotlin.runtime.client.AwsClientOption -import aws.smithy.kotlin.runtime.client.ExecutionContext +import aws.sdk.kotlin.runtime.ConfigurationException +import aws.smithy.kotlin.runtime.io.use +import aws.smithy.kotlin.runtime.util.PlatformProvider /** * Attempt to resolve the region to make requests to. - * - * Regions are resolved in the following order: - * 1. From the existing [ctx] - * 2. From the region [config] - * 3. Using default region detection (only if-enabled) */ -@InternalSdkApi -public suspend fun resolveRegionForOperation(ctx: ExecutionContext, config: RegionConfig): String { - // favor the context if it's already set - val regionFromCtx = ctx.getOrNull(AwsClientOption.Region) - if (regionFromCtx != null) return regionFromCtx - - // use the default from the service config if configured - if (config.region != null) return config.region!! - - // attempt to detect - val allowDefaultRegionDetect = ctx.getOrNull(AwsAdvancedClientOption.EnableDefaultRegionDetection) ?: true - if (!allowDefaultRegionDetect) { - throw ClientException("No region was configured and region detection has been disabled") +internal suspend fun resolveRegion(platformProvider: PlatformProvider): String = + DefaultRegionProviderChain(platformProvider).use { providerChain -> + providerChain.getRegion() ?: throw ConfigurationException("unable to auto detect AWS region, tried: $providerChain") } - - // TODO - propagate any relevant ctx/config to the default chain - val providerChain = DefaultRegionProviderChain() - return providerChain.getRegion() ?: throw ClientException("unable to auto detect AWS region, tried: $providerChain") -} diff --git a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/config/AwsClientConfigLoaderTest.kt b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/config/AwsClientConfigLoaderTest.kt new file mode 100644 index 00000000000..020d5a126b7 --- /dev/null +++ b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/config/AwsClientConfigLoaderTest.kt @@ -0,0 +1,28 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package aws.sdk.kotlin.runtime.config + +import aws.sdk.kotlin.runtime.auth.credentials.StaticCredentialsProvider +import aws.sdk.kotlin.runtime.testing.TestPlatformProvider +import aws.sdk.kotlin.runtime.testing.runSuspendTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class AwsClientConfigLoaderTest { + @Test + fun testExplicit(): Unit = runSuspendTest { + val provider = TestPlatformProvider() + val actual = loadAwsClientConfig(provider) { + region = "us-east-2" + credentialsProvider = StaticCredentialsProvider { + accessKeyId = "AKID" + secretAccessKey = "secret" + } + } + assertEquals("us-east-2", actual.region) + assertEquals("AKID", actual.credentialsProvider.getCredentials().accessKeyId) + } +} diff --git a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/region/ResolveRegionTest.kt b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/region/ResolveRegionTest.kt deleted file mode 100644 index 2cffb4e5a40..00000000000 --- a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/region/ResolveRegionTest.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -package aws.sdk.kotlin.runtime.region - -import aws.sdk.kotlin.runtime.client.AwsClientOption -import aws.sdk.kotlin.runtime.testing.runSuspendTest -import aws.smithy.kotlin.runtime.client.ExecutionContext -import kotlin.test.Test -import kotlin.test.assertEquals - -class ResolveRegionTest { - - @Test - fun testResolutionOrder() = runSuspendTest { - // from context - val config = object : RegionConfig { - override val region: String = "us-west-2" - } - - // context has highest priority - val actual = resolveRegionForOperation(ctx = ExecutionContext().apply { set(AwsClientOption.Region, "us-east-1") }, config) - assertEquals("us-east-1", actual) - - // from config is next - val actual2 = resolveRegionForOperation(ExecutionContext(), config) - assertEquals("us-west-2", actual2) - } -} diff --git a/aws-runtime/aws-config/jvm/src/aws/sdk/kotlin/runtime/region/DefaultRegionProviderChainJVM.kt b/aws-runtime/aws-config/jvm/src/aws/sdk/kotlin/runtime/region/DefaultRegionProviderChainJVM.kt index 0f864cb7d0b..a1673e284c9 100644 --- a/aws-runtime/aws-config/jvm/src/aws/sdk/kotlin/runtime/region/DefaultRegionProviderChainJVM.kt +++ b/aws-runtime/aws-config/jvm/src/aws/sdk/kotlin/runtime/region/DefaultRegionProviderChainJVM.kt @@ -8,7 +8,7 @@ package aws.sdk.kotlin.runtime.region import aws.smithy.kotlin.runtime.io.Closeable import aws.smithy.kotlin.runtime.util.PlatformProvider -public actual class DefaultRegionProviderChain public actual constructor( +internal actual class DefaultRegionProviderChain actual constructor( platformProvider: PlatformProvider ) : RegionProvider, Closeable, diff --git a/aws-runtime/aws-core/common/src/aws/sdk/kotlin/runtime/client/AwsAdvancedClientOption.kt b/aws-runtime/aws-core/common/src/aws/sdk/kotlin/runtime/client/AwsAdvancedClientOption.kt deleted file mode 100644 index 97a173b8c4c..00000000000 --- a/aws-runtime/aws-core/common/src/aws/sdk/kotlin/runtime/client/AwsAdvancedClientOption.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -package aws.sdk.kotlin.runtime.client - -import aws.smithy.kotlin.runtime.client.ClientOption - -/** - * A collection of advanced options that can be configured on an AWS service client - */ -public object AwsAdvancedClientOption { - /** - * Whether region detection should be enabled. Region detection is used when the region is not specified - * when building a client. This is enabled by default. - */ - public val EnableDefaultRegionDetection: ClientOption = ClientOption("EnableDefaultRegionDetection") -} diff --git a/aws-runtime/aws-types/common/src/aws/sdk/kotlin/runtime/auth/AuthConfig.kt b/aws-runtime/aws-types/common/src/aws/sdk/kotlin/runtime/auth/AuthConfig.kt deleted file mode 100644 index ca10df221b2..00000000000 --- a/aws-runtime/aws-types/common/src/aws/sdk/kotlin/runtime/auth/AuthConfig.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -package aws.sdk.kotlin.runtime.auth - -import aws.sdk.kotlin.runtime.auth.credentials.CredentialsProvider - -/** - * A common interface that all AWS service clients implement as part of their configuration state. - */ -public interface AuthConfig { - - /** - * Specifies zero or more credential providers that will be called to resolve credentials before making - * AWS service calls. If not provided a default credential provider chain is used. - */ - public val credentialsProvider: CredentialsProvider? - - /** - * AWS Region to be used for signing the request. This is not always same as `region` in case of global services. - */ - public val signingRegion: String? -} diff --git a/aws-runtime/aws-types/common/src/aws/sdk/kotlin/runtime/client/AwsClientConfig.kt b/aws-runtime/aws-types/common/src/aws/sdk/kotlin/runtime/client/AwsClientConfig.kt new file mode 100644 index 00000000000..e8f0f8c61a0 --- /dev/null +++ b/aws-runtime/aws-types/common/src/aws/sdk/kotlin/runtime/client/AwsClientConfig.kt @@ -0,0 +1,25 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package aws.sdk.kotlin.runtime.client + +import aws.sdk.kotlin.runtime.auth.credentials.CredentialsProvider + +/** + * Shared AWS service client configuration that all AWS service clients implement as part of their configuration state. + */ +public interface AwsClientConfig { + /** + * The AWS region to make requests to + */ + public val region: String + + /** + * The [CredentialsProvider] that will be called to resolve credentials before making AWS service calls + */ + public val credentialsProvider: CredentialsProvider + + public companion object {} +} diff --git a/aws-runtime/aws-types/common/src/aws/sdk/kotlin/runtime/region/RegionConfig.kt b/aws-runtime/aws-types/common/src/aws/sdk/kotlin/runtime/region/RegionConfig.kt deleted file mode 100644 index 1723ddd46ee..00000000000 --- a/aws-runtime/aws-types/common/src/aws/sdk/kotlin/runtime/region/RegionConfig.kt +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -package aws.sdk.kotlin.runtime.region - -/** - * A common interface that all AWS service clients implement as part of their configuration state. - */ -public interface RegionConfig { - /** - * AWS Region the client was configured with. Note that this is not always the signing region in the case of global - * services like IAM. - */ - public val region: String? -} diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt index 53e37628765..e62cc73e219 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt @@ -40,6 +40,7 @@ object AwsRuntimeTypes { object Types { val CredentialsProvider = runtimeSymbol("CredentialsProvider", AwsKotlinDependency.AWS_TYPES, "auth.credentials") val Credentials = runtimeSymbol("Credentials", AwsKotlinDependency.AWS_TYPES, "auth.credentials") + val AwsClientConfig = runtimeSymbol("AwsClientConfig", AwsKotlinDependency.AWS_TYPES, "client") } object Config { @@ -51,6 +52,9 @@ object AwsRuntimeTypes { val DefaultChainCredentialsProvider = runtimeSymbol("DefaultChainCredentialsProvider", AwsKotlinDependency.AWS_CONFIG, "auth.credentials") val StaticCredentialsProvider = runtimeSymbol("StaticCredentialsProvider", AwsKotlinDependency.AWS_CONFIG, "auth.credentials") } + + val AwsClientConfigLoadOptions = runtimeSymbol("AwsClientConfigLoadOptions", AwsKotlinDependency.AWS_CONFIG, "config") + val fromEnvironment = runtimeSymbol("fromEnvironment", AwsKotlinDependency.AWS_CONFIG, "config") } object Signing { diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsServiceConfigIntegration.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsServiceConfigIntegration.kt index 2a02c5915f7..da4c25bed70 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsServiceConfigIntegration.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsServiceConfigIntegration.kt @@ -4,11 +4,18 @@ */ package aws.sdk.kotlin.codegen +import aws.sdk.kotlin.codegen.protocols.core.EndpointResolverGenerator +import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.kotlin.codegen.core.* import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration +import software.amazon.smithy.kotlin.codegen.integration.SectionWriter +import software.amazon.smithy.kotlin.codegen.integration.SectionWriterBinding +import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes +import software.amazon.smithy.kotlin.codegen.model.boxed import software.amazon.smithy.kotlin.codegen.model.buildSymbol -import software.amazon.smithy.kotlin.codegen.model.namespace import software.amazon.smithy.kotlin.codegen.rendering.ClientConfigProperty +import software.amazon.smithy.kotlin.codegen.rendering.ClientConfigPropertyType +import software.amazon.smithy.kotlin.codegen.rendering.ServiceGenerator class AwsServiceConfigIntegration : KotlinIntegration { companion object { @@ -17,60 +24,106 @@ class AwsServiceConfigIntegration : KotlinIntegration { // AuthConfig properties val CredentialsProviderProp: ClientConfigProperty - val SigningRegionProp: ClientConfigProperty - - val EndpointResolverProp: ClientConfigProperty = ClientConfigProperty { - name = "endpointResolver" - documentation = """ - Determines the endpoint (hostname) to make requests to. When not provided a default - resolver is configured automatically. This is an advanced client option. - """.trimIndent() - - symbol = buildSymbol { - name = "EndpointResolver" - namespace(AwsKotlinDependency.AWS_CORE, subpackage = "endpoint") - } - } init { - val regionConfigSymbol = buildSymbol { - name = "RegionConfig" - namespace(AwsKotlinDependency.AWS_TYPES, subpackage = "region") - } - - RegionProp = ClientConfigProperty.String( - "region", + RegionProp = ClientConfigProperty { + name = "region" + symbol = KotlinTypes.String.toBuilder().boxed().build() + baseClass = AwsRuntimeTypes.Types.AwsClientConfig documentation = """ AWS region to make requests to - """.trimIndent(), - baseClass = regionConfigSymbol - ) - - val authConfigSymbol = buildSymbol { - name = "AuthConfig" - namespace(AwsKotlinDependency.AWS_TYPES, subpackage = "auth") + """.trimIndent() + propertyType = ClientConfigPropertyType.Required() } - SigningRegionProp = ClientConfigProperty.String( - "signingRegion", - documentation = """ - AWS region to be used for signing the request. This is not necessarily the same as `region` - in the case of global services like IAM - """.trimIndent(), - baseClass = authConfigSymbol - ) - CredentialsProviderProp = ClientConfigProperty { symbol = AwsRuntimeTypes.Types.CredentialsProvider - baseClass = authConfigSymbol + baseClass = AwsRuntimeTypes.Types.AwsClientConfig documentation = """ The AWS credentials provider to use for authenticating requests. If not provided a [${symbol?.namespace}.DefaultChainCredentialsProvider] instance will be used. """.trimIndent() + + val defaultProvider = AwsRuntimeTypes.Config.Credentials.DefaultChainCredentialsProvider + propertyType = ClientConfigPropertyType.RequiredWithDefault("${defaultProvider.name}()") + additionalImports = listOf(defaultProvider) + } + } + + private val overrideServiceCompanionObjectWriter = SectionWriter { writer, _ -> + // override the service client companion object for how a client is constructed + val serviceSymbol: Symbol = writer.getContextValue(ServiceGenerator.ServiceInterfaceCompanionObject.ServiceSymbol) + writer.withBlock("companion object {", "}") { + withBlock( + "operator fun invoke(sharedConfig: #T? = null, block: Config.DslBuilder.() -> Unit = {}): #L {", + "}", + AwsRuntimeTypes.Types.AwsClientConfig, + serviceSymbol.name + ) { + withBlock( + "val config = Config.BuilderImpl().apply { ", + "}.apply(block).build()" + ) { + write("region = sharedConfig?.region") + write("credentialsProvider = sharedConfig?.credentialsProvider") + } + write("return Default${serviceSymbol.name}(config)") + } + + write("") + write("operator fun invoke(config: Config): ${serviceSymbol.name} = Default${serviceSymbol.name}(config)") + + // generate a convenience init to resolve a client from the current environment + listOf( + AwsRuntimeTypes.Types.AwsClientConfig, + AwsRuntimeTypes.Config.AwsClientConfigLoadOptions, + AwsRuntimeTypes.Config.fromEnvironment + ).forEach(writer::addImport) + + write("") + dokka { + write("Construct a [${serviceSymbol.name}] by resolving the configuration from the current environment.") + write("NOTE: If you are using multiple AWS service clients you may wish to share the configuration among them") + write("by constructing a [#Q] and passing it to each client at construction.", AwsRuntimeTypes.Types.AwsClientConfig) + } + writer.withBlock( + "suspend fun fromEnvironment(block: #1T.() -> Unit = {}): #2T {", + "}", + AwsRuntimeTypes.Config.AwsClientConfigLoadOptions, + serviceSymbol + ) { + write( + "val sharedConfig = #T.#T(block)", + AwsRuntimeTypes.Types.AwsClientConfig, + AwsRuntimeTypes.Config.fromEnvironment + ) + write("return #T(sharedConfig)", serviceSymbol) + } } } } - override fun additionalServiceConfigProps(ctx: CodegenContext): List = - listOf(RegionProp, SigningRegionProp, CredentialsProviderProp, EndpointResolverProp) + override val sectionWriters: List = + listOf(SectionWriterBinding(ServiceGenerator.ServiceInterfaceCompanionObject, overrideServiceCompanionObjectWriter)) + + override fun additionalServiceConfigProps(ctx: CodegenContext): List { + // we can't construct this without the actual package name due to the generated DefaultEndpointResolver symbol + val endpointResolverProperty = ClientConfigProperty { + name = "endpointResolver" + documentation = """ + Determines the endpoint (hostname) to make requests to. When not provided a default + resolver is configured automatically. This is an advanced client option. + """.trimIndent() + + val defaultResolver = buildSymbol { + name = EndpointResolverGenerator.typeName + namespace = "${ctx.settings.pkg.name}.internal" + } + symbol = AwsRuntimeTypes.Endpoint.EndpointResolver + propertyType = ClientConfigPropertyType.RequiredWithDefault("${defaultResolver.name}()") + additionalImports = listOf(defaultResolver) + } + + return listOf(RegionProp, CredentialsProviderProp, endpointResolverProperty) + } } diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/PresignerGenerator.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/PresignerGenerator.kt index eb415e44fe8..5a471e7c570 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/PresignerGenerator.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/PresignerGenerator.kt @@ -26,14 +26,15 @@ import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes import software.amazon.smithy.kotlin.codegen.model.buildSymbol import software.amazon.smithy.kotlin.codegen.model.expectShape import software.amazon.smithy.kotlin.codegen.model.expectTrait -import software.amazon.smithy.kotlin.codegen.model.namespace import software.amazon.smithy.kotlin.codegen.rendering.ClientConfigGenerator import software.amazon.smithy.kotlin.codegen.rendering.ClientConfigProperty +import software.amazon.smithy.kotlin.codegen.rendering.ClientConfigPropertyType import software.amazon.smithy.kotlin.codegen.rendering.protocol.HttpBindingProtocolGenerator import software.amazon.smithy.kotlin.codegen.rendering.protocol.HttpBindingResolver import software.amazon.smithy.kotlin.codegen.rendering.protocol.HttpTraitResolver import software.amazon.smithy.kotlin.codegen.rendering.protocol.hasHttpBody import software.amazon.smithy.kotlin.codegen.rendering.serde.serializerName +import software.amazon.smithy.kotlin.codegen.utils.dq import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.model.shapes.ShapeId @@ -190,26 +191,18 @@ class PresignerGenerator : KotlinIntegration { writer.addImport(AwsRuntimeTypes.Core.ClientException) writer.putContext("configClass.name", presignConfigTypeName) val credentialsProviderProperty = ClientConfigProperty { - symbol = buildSymbol { - name = "CredentialsProvider" - defaultValue = "DefaultChainCredentialsProvider()" - namespace(AwsKotlinDependency.AWS_SIGNING) - nullable = false - } + symbol = AwsRuntimeTypes.Types.CredentialsProvider name = "credentialsProvider" documentation = "The AWS credentials provider to use for authenticating requests. If not provided a [aws.sdk.kotlin.runtime.auth.credentials.DefaultChainCredentialsProvider] instance will be used." baseClass = AwsRuntimeTypes.Signing.ServicePresignConfig + propertyType = ClientConfigPropertyType.RequiredWithDefault("DefaultChainCredentialsProvider()") } val endpointResolverProperty = ClientConfigProperty { - symbol = buildSymbol { - name = "EndpointResolver" - namespace(AwsKotlinDependency.AWS_CORE, "endpoint") - defaultValue = "DefaultEndpointResolver()" - nullable = false - } + symbol = AwsRuntimeTypes.Endpoint.EndpointResolver name = "endpointResolver" documentation = "Determines the endpoint (hostname) to make requests to. When not provided a default resolver is configured automatically. This is an advanced client option." baseClass = AwsRuntimeTypes.Signing.ServicePresignConfig + propertyType = ClientConfigPropertyType.RequiredWithDefault("DefaultEndpointResolver()") } val region = ClientConfigProperty { symbol = buildSymbol { @@ -220,21 +213,21 @@ class PresignerGenerator : KotlinIntegration { name = "region" documentation = "AWS region to make requests for" baseClass = AwsRuntimeTypes.Signing.ServicePresignConfig - required = true + propertyType = ClientConfigPropertyType.Required() } val signingNameProperty = ClientConfigProperty { symbol = KotlinTypes.String name = "signingName" documentation = "Service identifier used to sign requests" baseClass = AwsRuntimeTypes.Signing.ServicePresignConfig - constantValue = """"$sigv4ServiceName"""" + propertyType = ClientConfigPropertyType.ConstantValue(sigv4ServiceName.dq()) } val serviceIdProperty = ClientConfigProperty { symbol = KotlinTypes.String name = "serviceId" documentation = "Service identifier used to resolve endpoints" baseClass = AwsRuntimeTypes.Signing.ServicePresignConfig - constantValue = """"$serviceId"""" + propertyType = ClientConfigPropertyType.ConstantValue(serviceId.dq()) } val ccg = ClientConfigGenerator( @@ -325,31 +318,31 @@ class PresignerGenerator : KotlinIntegration { ) { writer.dokka { write("Presign a [$requestTypeName] using a [$serviceClientTypeName].") - write("@param serviceClient the client providing properties used to generate the presigned request.") + write("@param config the client configuration used to generate the presigned request.") write("@param durationSeconds the amount of time from signing for which the request is valid, with seconds granularity.") write("@return The [HttpRequest] that can be invoked within the specified time window.") } // FIXME ~ Replace or add additional function, swap ULong type for kotlin.time.Duration when type becomes stable - writer.withBlock("suspend fun $requestTypeName.presign(serviceClient: $serviceClientTypeName, durationSeconds: ULong): HttpRequest {", "}\n") { - withBlock("val serviceClientConfig = $presignConfigTypeName {", "}") { - write("credentialsProvider = serviceClient.config.credentialsProvider ?: DefaultChainCredentialsProvider()") - write("endpointResolver = serviceClient.config.endpointResolver ?: DefaultEndpointResolver()") - write("region = requireNotNull(serviceClient.config.region) { \"Service client must set a region.\" }") + writer.withBlock("suspend fun $requestTypeName.presign(config: $serviceClientTypeName.Config, durationSeconds: ULong): HttpRequest {", "}\n") { + withBlock("val presignConfig = $presignConfigTypeName {", "}") { + write("credentialsProvider = config.credentialsProvider") + write("endpointResolver = config.endpointResolver") + write("region = config.region") } - write("return createPresignedRequest(serviceClientConfig, $requestConfigFnName(this, durationSeconds))") + write("return createPresignedRequest(presignConfig, $requestConfigFnName(this, durationSeconds))") } } private fun renderPresignFromConfigFn(writer: KotlinWriter, requestTypeName: String, requestConfigFnName: String) { writer.dokka { write("Presign a [$requestTypeName] using a [ServicePresignConfig].") - write("@param serviceClientConfig the client configuration used to generate the presigned request") + write("@param presignConfig the configuration used to generate the presigned request") write("@param durationSeconds the amount of time from signing for which the request is valid, with seconds granularity.") write("@return The [HttpRequest] that can be invoked within the specified time window.") } // FIXME ~ Replace or add additional function, swap ULong type for kotlin.time.Duration when type becomes stable - writer.withBlock("suspend fun $requestTypeName.presign(serviceClientConfig: ServicePresignConfig, durationSeconds: ULong): HttpRequest {", "}\n") { - write("return createPresignedRequest(serviceClientConfig, $requestConfigFnName(this, durationSeconds))") + writer.withBlock("suspend fun $requestTypeName.presign(presignConfig: ServicePresignConfig, durationSeconds: ULong): HttpRequest {", "}\n") { + write("return createPresignedRequest(presignConfig, $requestConfigFnName(this, durationSeconds))") } } diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/protocols/core/AwsHttpProtocolClientGenerator.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/protocols/core/AwsHttpProtocolClientGenerator.kt index 9ca0f0567f7..ede50d006f5 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/protocols/core/AwsHttpProtocolClientGenerator.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/protocols/core/AwsHttpProtocolClientGenerator.kt @@ -77,16 +77,14 @@ open class AwsHttpProtocolClientGenerator( // FIXME - we also need a way to tie in config properties added via integrations that need to influence the context writer.addImport(RuntimeTypes.Core.ExecutionContext) writer.addImport("SdkClientOption", KotlinDependency.CORE, "${KotlinDependency.CORE.namespace}.client") - writer.addImport(AwsRuntimeTypes.Config.Region.resolveRegionForOperation) writer.addImport(AwsRuntimeTypes.Core.AuthAttributes) writer.addImport(AwsRuntimeTypes.Core.AwsClientOption) writer.addImport("putIfAbsent", KotlinDependency.UTILS) writer.dokka("merge the defaults configured for the service into the execution context before firing off a request") writer.openBlock("private suspend fun mergeServiceDefaults(ctx: ExecutionContext) {", "}") { - writer.write("val region = #T(ctx, config)", AwsRuntimeTypes.Config.Region.resolveRegionForOperation) - writer.write("ctx.putIfAbsent(AwsClientOption.Region, region)") - writer.write("ctx.putIfAbsent(AuthAttributes.SigningRegion, config.signingRegion ?: region)") + writer.write("ctx.putIfAbsent(AwsClientOption.Region, config.region)") + writer.write("ctx.putIfAbsent(AuthAttributes.SigningRegion, config.region)") writer.write("ctx.putIfAbsent(SdkClientOption.ServiceName, serviceName)") writer.write("ctx.putIfAbsent(SdkClientOption.LogMode, config.sdkLogMode)") diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/protocols/middleware/AwsSignatureVersion4.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/protocols/middleware/AwsSignatureVersion4.kt index 0c2f46b6200..3d19411ebb5 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/protocols/middleware/AwsSignatureVersion4.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/protocols/middleware/AwsSignatureVersion4.kt @@ -39,9 +39,8 @@ open class AwsSignatureVersion4(private val signingServiceName: String) : HttpFe override fun renderConfigure(writer: KotlinWriter) { writer.addImport(AwsRuntimeTypes.Signing.AwsSigV4SigningMiddleware) - writer.addImport(AwsRuntimeTypes.Config.Credentials.DefaultChainCredentialsProvider) - writer.write("this.credentialsProvider = config.credentialsProvider ?: DefaultChainCredentialsProvider()") + writer.write("this.credentialsProvider = config.credentialsProvider") writer.write("this.signingService = #S", signingServiceName) } diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/protocols/middleware/EndpointResolverMiddleware.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/protocols/middleware/EndpointResolverMiddleware.kt index 04a06edeba0..8e079779435 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/protocols/middleware/EndpointResolverMiddleware.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/protocols/middleware/EndpointResolverMiddleware.kt @@ -22,17 +22,9 @@ class EndpointResolverMiddleware(private val ctx: ProtocolGenerator.GenerationCo name = "ServiceEndpointResolver" namespace(AwsKotlinDependency.AWS_HTTP, subpackage = "middleware") } - - // generated symbol - val defaultResolverSymbol = buildSymbol { - name = "DefaultEndpointResolver" - namespace = "${ctx.settings.pkg.name}.internal" - } - writer.addImport(resolverFeatureSymbol) - writer.addImport(defaultResolverSymbol) writer.write("serviceId = ServiceId") - writer.write("resolver = config.endpointResolver ?: DefaultEndpointResolver()") + writer.write("resolver = config.endpointResolver") } } diff --git a/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/aws/sdk/kotlin/codegen/AwsServiceConfigIntegrationTest.kt b/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/aws/sdk/kotlin/codegen/AwsServiceConfigIntegrationTest.kt new file mode 100644 index 00000000000..07be8f95946 --- /dev/null +++ b/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/aws/sdk/kotlin/codegen/AwsServiceConfigIntegrationTest.kt @@ -0,0 +1,59 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package aws.sdk.kotlin.codegen + +import org.junit.jupiter.api.Test +import software.amazon.smithy.kotlin.codegen.core.KotlinWriter +import software.amazon.smithy.kotlin.codegen.model.expectShape +import software.amazon.smithy.kotlin.codegen.rendering.ClientConfigGenerator +import software.amazon.smithy.kotlin.codegen.test.* +import software.amazon.smithy.model.shapes.ServiceShape + +class AwsServiceConfigIntegrationTest { + @Test + fun testServiceConfigurationProperties() { + val model = """ + namespace com.test + + use aws.protocols#awsJson1_1 + use aws.api#service + + @service(sdkId: "service with overrides", endpointPrefix: "service-with-overrides") + @awsJson1_1 + service Example { + version: "1.0.0", + operations: [GetFoo] + } + + operation GetFoo {} + """.toSmithyModel() + + val serviceShape = model.expectShape("com.test#Example") + + val testCtx = model.newTestContext(serviceName = "Example") + val writer = KotlinWriter("com.test") + + val renderingCtx = testCtx.toRenderingContext(writer, serviceShape) + .copy(integrations = listOf(AwsServiceConfigIntegration())) + + ClientConfigGenerator(renderingCtx, detectDefaultProps = false).render() + val contents = writer.toString() + + val expectedProps = """ + override val credentialsProvider: CredentialsProvider = builder.credentialsProvider ?: DefaultChainCredentialsProvider() + val endpointResolver: EndpointResolver = builder.endpointResolver ?: DefaultEndpointResolver() + override val region: String = requireNotNull(builder.region) { "region is a required configuration property" } +""" + contents.shouldContainOnlyOnceWithDiff(expectedProps) + + val expectedImpl = """ + override var credentialsProvider: CredentialsProvider? = null + override var endpointResolver: EndpointResolver? = null + override var region: String? = null +""" + contents.shouldContainOnlyOnceWithDiff(expectedImpl) + } +} diff --git a/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/aws/sdk/kotlin/codegen/PresignerGeneratorTest.kt b/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/aws/sdk/kotlin/codegen/PresignerGeneratorTest.kt index 7118d818a4e..65368882fd8 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/aws/sdk/kotlin/codegen/PresignerGeneratorTest.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/aws/sdk/kotlin/codegen/PresignerGeneratorTest.kt @@ -107,27 +107,27 @@ class PresignerGeneratorTest { /** * Presign a [GetFooRequest] using a [ServicePresignConfig]. - * @param serviceClientConfig the client configuration used to generate the presigned request + * @param presignConfig the configuration used to generate the presigned request * @param durationSeconds the amount of time from signing for which the request is valid, with seconds granularity. * @return The [HttpRequest] that can be invoked within the specified time window. */ - suspend fun GetFooRequest.presign(serviceClientConfig: ServicePresignConfig, durationSeconds: ULong): HttpRequest { - return createPresignedRequest(serviceClientConfig, getFooPresignConfig(this, durationSeconds)) + suspend fun GetFooRequest.presign(presignConfig: ServicePresignConfig, durationSeconds: ULong): HttpRequest { + return createPresignedRequest(presignConfig, getFooPresignConfig(this, durationSeconds)) } /** * Presign a [GetFooRequest] using a [TestClient]. - * @param serviceClient the client providing properties used to generate the presigned request. + * @param config the client configuration used to generate the presigned request. * @param durationSeconds the amount of time from signing for which the request is valid, with seconds granularity. * @return The [HttpRequest] that can be invoked within the specified time window. */ - suspend fun GetFooRequest.presign(serviceClient: TestClient, durationSeconds: ULong): HttpRequest { - val serviceClientConfig = TestPresignConfig { - credentialsProvider = serviceClient.config.credentialsProvider ?: DefaultChainCredentialsProvider() - endpointResolver = serviceClient.config.endpointResolver ?: DefaultEndpointResolver() - region = requireNotNull(serviceClient.config.region) { "Service client must set a region." } + suspend fun GetFooRequest.presign(config: TestClient.Config, durationSeconds: ULong): HttpRequest { + val presignConfig = TestPresignConfig { + credentialsProvider = config.credentialsProvider + endpointResolver = config.endpointResolver + region = config.region } - return createPresignedRequest(serviceClientConfig, getFooPresignConfig(this, durationSeconds)) + return createPresignedRequest(presignConfig, getFooPresignConfig(this, durationSeconds)) } private suspend fun getFooPresignConfig(input: GetFooRequest, durationSeconds: ULong) : PresignedRequestConfig { @@ -145,27 +145,27 @@ class PresignerGeneratorTest { /** * Presign a [PostFooRequest] using a [ServicePresignConfig]. - * @param serviceClientConfig the client configuration used to generate the presigned request + * @param presignConfig the configuration used to generate the presigned request * @param durationSeconds the amount of time from signing for which the request is valid, with seconds granularity. * @return The [HttpRequest] that can be invoked within the specified time window. */ - suspend fun PostFooRequest.presign(serviceClientConfig: ServicePresignConfig, durationSeconds: ULong): HttpRequest { - return createPresignedRequest(serviceClientConfig, postFooPresignConfig(this, durationSeconds)) + suspend fun PostFooRequest.presign(presignConfig: ServicePresignConfig, durationSeconds: ULong): HttpRequest { + return createPresignedRequest(presignConfig, postFooPresignConfig(this, durationSeconds)) } /** * Presign a [PostFooRequest] using a [TestClient]. - * @param serviceClient the client providing properties used to generate the presigned request. + * @param config the client configuration used to generate the presigned request. * @param durationSeconds the amount of time from signing for which the request is valid, with seconds granularity. * @return The [HttpRequest] that can be invoked within the specified time window. */ - suspend fun PostFooRequest.presign(serviceClient: TestClient, durationSeconds: ULong): HttpRequest { - val serviceClientConfig = TestPresignConfig { - credentialsProvider = serviceClient.config.credentialsProvider ?: DefaultChainCredentialsProvider() - endpointResolver = serviceClient.config.endpointResolver ?: DefaultEndpointResolver() - region = requireNotNull(serviceClient.config.region) { "Service client must set a region." } + suspend fun PostFooRequest.presign(config: TestClient.Config, durationSeconds: ULong): HttpRequest { + val presignConfig = TestPresignConfig { + credentialsProvider = config.credentialsProvider + endpointResolver = config.endpointResolver + region = config.region } - return createPresignedRequest(serviceClientConfig, postFooPresignConfig(this, durationSeconds)) + return createPresignedRequest(presignConfig, postFooPresignConfig(this, durationSeconds)) } private suspend fun postFooPresignConfig(input: PostFooRequest, durationSeconds: ULong) : PresignedRequestConfig { @@ -183,27 +183,27 @@ class PresignerGeneratorTest { /** * Presign a [PutFooRequest] using a [ServicePresignConfig]. - * @param serviceClientConfig the client configuration used to generate the presigned request + * @param presignConfig the configuration used to generate the presigned request * @param durationSeconds the amount of time from signing for which the request is valid, with seconds granularity. * @return The [HttpRequest] that can be invoked within the specified time window. */ - suspend fun PutFooRequest.presign(serviceClientConfig: ServicePresignConfig, durationSeconds: ULong): HttpRequest { - return createPresignedRequest(serviceClientConfig, putFooPresignConfig(this, durationSeconds)) + suspend fun PutFooRequest.presign(presignConfig: ServicePresignConfig, durationSeconds: ULong): HttpRequest { + return createPresignedRequest(presignConfig, putFooPresignConfig(this, durationSeconds)) } /** * Presign a [PutFooRequest] using a [TestClient]. - * @param serviceClient the client providing properties used to generate the presigned request. + * @param config the client configuration used to generate the presigned request. * @param durationSeconds the amount of time from signing for which the request is valid, with seconds granularity. * @return The [HttpRequest] that can be invoked within the specified time window. */ - suspend fun PutFooRequest.presign(serviceClient: TestClient, durationSeconds: ULong): HttpRequest { - val serviceClientConfig = TestPresignConfig { - credentialsProvider = serviceClient.config.credentialsProvider ?: DefaultChainCredentialsProvider() - endpointResolver = serviceClient.config.endpointResolver ?: DefaultEndpointResolver() - region = requireNotNull(serviceClient.config.region) { "Service client must set a region." } + suspend fun PutFooRequest.presign(config: TestClient.Config, durationSeconds: ULong): HttpRequest { + val presignConfig = TestPresignConfig { + credentialsProvider = config.credentialsProvider + endpointResolver = config.endpointResolver + region = config.region } - return createPresignedRequest(serviceClientConfig, putFooPresignConfig(this, durationSeconds)) + return createPresignedRequest(presignConfig, putFooPresignConfig(this, durationSeconds)) } private suspend fun putFooPresignConfig(input: PutFooRequest, durationSeconds: ULong) : PresignedRequestConfig { @@ -225,15 +225,15 @@ class PresignerGeneratorTest { * instance is not available. */ class TestPresignConfig private constructor(builder: BuilderImpl): ServicePresignConfig { - override val credentialsProvider: CredentialsProvider = builder.credentialsProvider - override val endpointResolver: EndpointResolver = builder.endpointResolver - override val region: String = builder.region ?: throw ClientException("region must be set") + override val credentialsProvider: CredentialsProvider = builder.credentialsProvider ?: DefaultChainCredentialsProvider() + override val endpointResolver: EndpointResolver = builder.endpointResolver ?: DefaultEndpointResolver() + override val region: String = requireNotNull(builder.region) { "region is a required configuration property" } override val serviceId: String = "example" override val signingName: String = "example-signing-name" companion object { @JvmStatic fun fluentBuilder(): FluentBuilder = BuilderImpl() - fun builder(): DslBuilder = BuilderImpl() + operator fun invoke(block: DslBuilder.() -> kotlin.Unit): ServicePresignConfig = BuilderImpl().apply(block).build() } @@ -248,24 +248,23 @@ class PresignerGeneratorTest { /** * The AWS credentials provider to use for authenticating requests. If not provided a [aws.sdk.kotlin.runtime.auth.credentials.DefaultChainCredentialsProvider] instance will be used. */ - var credentialsProvider: CredentialsProvider + var credentialsProvider: CredentialsProvider? /** * Determines the endpoint (hostname) to make requests to. When not provided a default resolver is configured automatically. This is an advanced client option. */ - var endpointResolver: EndpointResolver + var endpointResolver: EndpointResolver? /** * AWS region to make requests for */ var region: String? - fun build(): TestPresignConfig } internal class BuilderImpl() : FluentBuilder, DslBuilder { - override var credentialsProvider: CredentialsProvider = DefaultChainCredentialsProvider() - override var endpointResolver: EndpointResolver = DefaultEndpointResolver() + override var credentialsProvider: CredentialsProvider? = null + override var endpointResolver: EndpointResolver? = null override var region: String? = null override fun build(): TestPresignConfig = TestPresignConfig(this)