aws-sdk-kotlin icon indicating copy to clipboard operation
aws-sdk-kotlin copied to clipboard

Configuration via SPI on JVM

Open aajtodd opened this issue 3 years ago • 4 comments

Describe the feature

It would be useful to allow auto registration of various SDK configuration on the JVM via SPI (ServiceLoader).

Possible candidates:

  • HTTP engine
  • Tracing probe (once available)
  • Interceptors (once designed and available)

Is your Feature Request related to a problem?

Today customers have to change their code to swap out HTTP engine, insert tracing, or register interceptors (NOTE: some of these are features planned but not yet available today). It would be extremely useful for customers to swap out or install some of these components without requiring code changes (e.g. to test HTTP engine differences, instrument their application, etc).

Proposed Solution

Investigate SPI loading mechanism in Java V2 SDK to see what they allow to be configured this way and determine what (if any) we would want equivalents of. This would be a JVM only feature and require runtime support.

Describe alternative solutions or features you've considered

No response

Acknowledge

  • [X] I may be able to implement this feature request

AWS Kotlin SDK version used

N/A

Platform (JVM/JS/Native)

JVM

Operating System and version

N/A

aajtodd avatar Oct 06 '22 16:10 aajtodd

SPI is one possible option, but a bit magical for some cases (random jar on class path changes behaviour). It would be great to not fully internalize all that and provide more flexible options.

For AWS Java SDK have built a Factory object that is exposed as a Spring bean; at configuration time we create various SDK clients via factory.build<S3Client, S3Client.Builder>() - which allows for various customizers to be applied to the builder (those are all registered with the factory).

Working through something similar with Kotlin SDK - its much more challenging as there aren't fixed super-types for the generated builder classes.

Related value from having a Factory concept - consistently providing configuration such as allowed credentials providers; centralized ability to generate/apply scoped-down policies for SaaS applications via StsAssumeRoleCredentialsProvider.

cloudshiftchris avatar Jul 28 '23 20:07 cloudshiftchris

Every client config/builder implements SdkClient/SdkClient.Builder and every client config implements SdkClientConfig/SdkClientConfig.Builder. Everything else is a mixin configuration (e.g. HTTP, identity, observability, etc).

I think what you are looking for may be available already, just a bit different than Java. Take a look at the Example Client Usage from https://github.com/awslabs/aws-sdk-kotlin/pull/814, specifically the vending machine example (c5) and let me know if that's closer to what you are after.

aajtodd avatar Jul 31 '23 12:07 aajtodd

Nice, thanks, that example address the factory issue - creating a builder uniformly (which I can now remove some reflective code for).

It still requires dispatch to each of the mix-ins but that is manageable.

An example in the docs would be helpful ("Centralizing creation / configuration of SDK clients" perhaps)

cloudshiftchris avatar Jul 31 '23 13:07 cloudshiftchris

fyi, end up with this implementation

private fun <
            TConfig : SdkClientConfig,
            TConfigBuilder : SdkClientConfig.Builder<TConfig>,
            TClient : SdkClient,
            TClientBuilder : SdkClient.Builder<TConfig, TConfigBuilder, TClient>> internalCreate(
        factory: SdkClientFactory<TConfig, TConfigBuilder, TClient, TClientBuilder>,
        customizers: List<AwsKotlinSdkClientCustomizer>,
        block: TConfigBuilder.() -> Unit
    ): TClient {

        val builder = factory.builder()
        val config = builder.config

        if (config is AwsSdkClientConfig.Builder) customizers.map { it.awsSdkClientConfigBuilder }
            .forEach(config::apply)
        if (config is CredentialsProviderConfig.Builder) customizers.map { it.credentialsProviderConfigBuilder }
            .forEach(config::apply)
        if (config is HttpAuthConfig.Builder) customizers.map { it.httpAuthConfigBuilder }.forEach(config::apply)
        if (config is HttpClientConfig.Builder) customizers.map { it.httpClientConfigBuilder }.forEach(config::apply)
        if (config is IdempotencyTokenConfig.Builder) customizers.map { it.idempotencyTokenConfigBuilder }
            .forEach(config::apply)
        if (config is RetryStrategyClientConfig.Builder) customizers.map { it.retryStrategyClientConfigBuilder }
            .forEach(config::apply)
        if (config is TelemetryConfig.Builder) customizers.map { it.telemetryConfigBuilder }.forEach(config::apply)

        builder.config.apply(block)
        return builder.build()
    }

...combined with creating customizers:

awsKotlinSdkClientCustomizer {
    awsSdkClient {
        useDualStack = props.useDualStack
        useFips = props.useFips
        this.region = region
    }
    credentialsProvider {
        credentialsProvider = effectiveCredentialsProvider
    }
}

Which all comes together for consumers to use a singleton factory w/ registered customizers to create SDK clients:

val stsClient = factory.create(StsClient) {
    // optional at-creation-time client-specific configuration
}

cloudshiftchris avatar Jul 31 '23 14:07 cloudshiftchris