From faaa39cdc2bb38fb433271a7737bf04e65949847 Mon Sep 17 00:00:00 2001 From: "ayeshmantha.perera" Date: Tue, 26 Aug 2025 21:07:12 +0200 Subject: [PATCH] S3 Autoconfiguration supports DefaultCredentialProvider. --- .../autoconfigure/s3/S3AutoConfiguration.java | 8 +- .../s3/S3AutoConfigurationTests.java | 184 ++++++++++++++++++ 2 files changed, 188 insertions(+), 4 deletions(-) diff --git a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3AutoConfiguration.java b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3AutoConfiguration.java index c72d2727f..4bf41322c 100644 --- a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3AutoConfiguration.java +++ b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3AutoConfiguration.java @@ -17,10 +17,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.awspring.cloud.autoconfigure.AwsSyncClientCustomizer; -import io.awspring.cloud.autoconfigure.core.AwsClientBuilderConfigurer; -import io.awspring.cloud.autoconfigure.core.AwsClientCustomizer; -import io.awspring.cloud.autoconfigure.core.AwsConnectionDetails; -import io.awspring.cloud.autoconfigure.core.AwsProperties; +import io.awspring.cloud.autoconfigure.core.*; import io.awspring.cloud.autoconfigure.s3.properties.S3Properties; import io.awspring.cloud.s3.InMemoryBufferingS3OutputStreamProvider; import io.awspring.cloud.s3.Jackson2JsonS3ObjectConverter; @@ -34,6 +31,7 @@ import java.util.Optional; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.*; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.PropertyMapper; @@ -56,11 +54,13 @@ * * @author Maciej Walkowiak * @author Matej Nedic + * @author Ayeshmantha Perera */ @AutoConfiguration @ConditionalOnClass({ S3Client.class, S3OutputStreamProvider.class }) @EnableConfigurationProperties({ S3Properties.class, AwsProperties.class }) @Import(S3ProtocolResolver.class) +@AutoConfigureAfter({ CredentialsProviderAutoConfiguration.class, RegionProviderAutoConfiguration.class, AwsAutoConfiguration.class }) @ConditionalOnProperty(name = "spring.cloud.aws.s3.enabled", havingValue = "true", matchIfMissing = true) public class S3AutoConfiguration { diff --git a/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/S3AutoConfigurationTests.java b/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/S3AutoConfigurationTests.java index 2d3b060e3..ea189fba0 100644 --- a/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/S3AutoConfigurationTests.java +++ b/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/S3AutoConfigurationTests.java @@ -22,7 +22,9 @@ import io.awspring.cloud.autoconfigure.ConfiguredAwsClient; import io.awspring.cloud.autoconfigure.ConfiguredAwsPresigner; import io.awspring.cloud.autoconfigure.core.AwsAutoConfiguration; +import io.awspring.cloud.autoconfigure.core.AwsClientBuilderConfigurer; import io.awspring.cloud.autoconfigure.core.AwsClientCustomizer; +import io.awspring.cloud.autoconfigure.core.AwsProperties; import io.awspring.cloud.autoconfigure.core.CredentialsProviderAutoConfiguration; import io.awspring.cloud.autoconfigure.core.RegionProviderAutoConfiguration; import io.awspring.cloud.autoconfigure.s3.properties.S3Properties; @@ -41,18 +43,26 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.beans.factory.UnsatisfiedDependencyException; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.lang.Nullable; import org.springframework.test.util.ReflectionTestUtils; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider; +import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider; +import software.amazon.awssdk.services.sts.auth.StsWebIdentityTokenFileCredentialsProvider; import software.amazon.awssdk.awscore.defaultsmode.DefaultsMode; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; import software.amazon.awssdk.core.client.config.SdkClientOption; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.apache.ApacheHttpClient; import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.regions.providers.AwsRegionProvider; import software.amazon.awssdk.s3accessgrants.plugin.S3AccessGrantsIdentityProvider; import software.amazon.awssdk.s3accessgrants.plugin.S3AccessGrantsPlugin; import software.amazon.awssdk.services.s3.S3Client; @@ -67,6 +77,7 @@ * @author Maciej Walkowiak * @author Matej Nedic * @author Giacomo Baso + * @author Ayeshmantha Perera */ class S3AutoConfigurationTests { @@ -445,6 +456,179 @@ S3OutputStreamProvider customS3OutputStreamProvider() { } + @Nested + class AutoConfigurationOrderingTests { + + @Test + void awsAutoConfigurationCreatesRequiredBeansBeforeS3() { + new ApplicationContextRunner() + .withPropertyValues("spring.cloud.aws.region.static:eu-west-1") + .withConfiguration(AutoConfigurations.of( + CredentialsProviderAutoConfiguration.class, + RegionProviderAutoConfiguration.class, + AwsAutoConfiguration.class + )) + .run(context -> { + assertThat(context).hasSingleBean(AwsCredentialsProvider.class); + assertThat(context).hasSingleBean(AwsRegionProvider.class); + assertThat(context).hasSingleBean(AwsClientBuilderConfigurer.class); + }); + } + + @Test + void s3AutoConfigurationUsesAwsClientBuilderConfigurer() { + contextRunner.run(context -> { + assertThat(context).hasSingleBean(AwsCredentialsProvider.class); + assertThat(context).hasSingleBean(AwsRegionProvider.class); + assertThat(context).hasSingleBean(AwsClientBuilderConfigurer.class); + assertThat(context).hasSingleBean(S3ClientBuilder.class); + assertThat(context).hasSingleBean(S3Client.class); + + AwsClientBuilderConfigurer configurer = context.getBean(AwsClientBuilderConfigurer.class); + S3ClientBuilder s3Builder = context.getBean(S3ClientBuilder.class); + + assertThat(configurer).isNotNull(); + assertThat(s3Builder).isNotNull(); + }); + } + + @Test + void s3ClientBuilderReceivesProperlyConfiguredAwsClientBuilderConfigurer() { + contextRunner + .withPropertyValues( + "spring.cloud.aws.region.static:eu-central-1", + "spring.cloud.aws.credentials.access-key:config-test-key", + "spring.cloud.aws.credentials.secret-key:config-test-secret" + ) + .run(context -> { + S3Client s3Client = context.getBean(S3Client.class); + + ConfiguredAwsClient client = new ConfiguredAwsClient(s3Client); + assertThat(client.getRegion()).isEqualTo(Region.of("eu-central-1")); + assertThat(client.getAwsCredentialsProvider()).isNotNull(); + }); + } + + @Test + void s3AutoConfigurationFailsGracefullyWithoutAwsAutoConfiguration() { + new ApplicationContextRunner() + .withPropertyValues("spring.cloud.aws.region.static:eu-west-1") + .withConfiguration(AutoConfigurations.of( + CredentialsProviderAutoConfiguration.class, + RegionProviderAutoConfiguration.class, + S3AutoConfiguration.class + )) + .run(context -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()) + .hasMessageContaining("AwsClientBuilderConfigurer") + .isInstanceOf(UnsatisfiedDependencyException.class); + }); + } + } + + @Nested + class CredentialTypesTests { + + @Test + void s3ClientSupportsWebIdentityTokenCredentials() { + // Simulate EKS Web Identity Token environment + contextRunner + .withSystemProperties( + "aws.webIdentityTokenFile", "/tmp/test-token", + "aws.roleArn", "arn:aws:iam::123456789012:role/test-eks-role", + "aws.roleSessionName", "test-session" + ) + .withPropertyValues("spring.cloud.aws.region.static:us-east-1") + .run(context -> { + assertThat(context).hasSingleBean(AwsCredentialsProvider.class); + assertThat(context).hasSingleBean(S3Client.class); + + AwsCredentialsProvider provider = context.getBean(AwsCredentialsProvider.class); + assertThat(provider).isNotNull() + .isInstanceOf(StsWebIdentityTokenFileCredentialsProvider.class); + + S3Client s3Client = context.getBean(S3Client.class); + assertThat(s3Client).isNotNull(); + }); + } + + @Test + void s3ClientWithInstanceProfileCredentials() { + contextRunner + .withPropertyValues( + "spring.cloud.aws.region.static:us-east-1", + "spring.cloud.aws.credentials.instance-profile:true" + ) + .run(context -> { + assertThat(context).hasSingleBean(AwsCredentialsProvider.class); + assertThat(context).hasSingleBean(S3Client.class); + + AwsCredentialsProvider provider = context.getBean(AwsCredentialsProvider.class); + assertThat(provider).isNotNull() + .isInstanceOf(InstanceProfileCredentialsProvider.class); + + S3Client s3Client = context.getBean(S3Client.class); + assertThat(s3Client).isNotNull(); + }); + } + + @Test + void s3ClientWithProfileBasedCredentials() { + contextRunner + .withPropertyValues( + "spring.cloud.aws.region.static:us-east-1", + "spring.cloud.aws.credentials.profile.name:test-profile" + ) + .run(context -> { + assertThat(context).hasSingleBean(AwsCredentialsProvider.class); + assertThat(context).hasSingleBean(S3Client.class); + + AwsCredentialsProvider provider = context.getBean(AwsCredentialsProvider.class); + assertThat(provider).isNotNull() + .isInstanceOf(ProfileCredentialsProvider.class); + + S3Client s3Client = context.getBean(S3Client.class); + assertThat(s3Client).isNotNull(); + }); + } + } + + @Nested + class CustomConfigurationTests { + + @Test + void customAwsClientBuilderConfigurerIsRespected() { + contextRunner + .withUserConfiguration(CustomAwsClientBuilderConfigurerConfiguration.class) + .run(context -> { + assertThat(context).hasBean("awsClientBuilderConfigurer"); + assertThat(context).hasBean("customAwsClientBuilderConfigurer"); + assertThat(context).hasSingleBean(S3Client.class); + + AwsClientBuilderConfigurer configurer = context.getBean(AwsClientBuilderConfigurer.class); + assertThat(configurer).isInstanceOf(CustomAwsClientBuilderConfigurerConfiguration.TestAwsClientBuilderConfigurer.class); + }); + } + + @TestConfiguration + static class CustomAwsClientBuilderConfigurerConfiguration { + @Bean + @Primary + AwsClientBuilderConfigurer customAwsClientBuilderConfigurer(AwsCredentialsProvider credentialsProvider, + AwsRegionProvider regionProvider, AwsProperties awsProperties) { + return new TestAwsClientBuilderConfigurer(credentialsProvider, regionProvider, awsProperties); + } + + static class TestAwsClientBuilderConfigurer extends AwsClientBuilderConfigurer { + public TestAwsClientBuilderConfigurer(AwsCredentialsProvider credentialsProvider, + AwsRegionProvider regionProvider, AwsProperties awsProperties) { + super(credentialsProvider, regionProvider, awsProperties); + } + } + } + } + private static AttributeMap.Builder resolveAttributeMap(S3ClientBuilder s3ClientBuilder) { AttributeMap.Builder attributes = (AttributeMap.Builder) ReflectionTestUtils .getField(ReflectionTestUtils.getField(s3ClientBuilder, "clientConfiguration"), "attributes");