diff --git a/docs/src/main/asciidoc/s3.adoc b/docs/src/main/asciidoc/s3.adoc index 6a9fc318a..9391dff14 100644 --- a/docs/src/main/asciidoc/s3.adoc +++ b/docs/src/main/asciidoc/s3.adoc @@ -604,17 +604,51 @@ The Spring Boot Starter for S3 provides the following configuration options: | `spring.cloud.aws.s3.config.reload.max-wait-time-for-restart` | `Duration`| `2s` | The maximum time between the detection of changes in property source and the application context restart when `restart_context` strategy is used. |=== +=== S3 Vector Client support + +https://aws.amazon.com/blogs/aws/introducing-amazon-s3-vectors-first-cloud-storage-with-native-vector-support-at-scale/[S3 Vector Store] is a vector storage which supports uploading, storing and querying vectors. +To allow users simpler use of `S3VectorsClient` with https://github.com/spring-projects/spring-ai[Spring AI] project or use of a plain client, we support autoconfiguration of the `S3VectorsClient`. + +To enable autoconfiguration you should add the following dependencies +[source,xml] +---- + + io.awspring.cloud + spring-cloud-aws-starter + + + + software.amazon.awssdk + s3vectors + +---- + +After dependencies are introduced Spring Cloud AWS will automatically create a `S3VectorsClient` bean which can than be autowired and used. + +[cols="2,3,1,1"] +|=== +| Name | Description | Required | Default value +| `spring.cloud.aws.s3.vector.config.enabled` | Enables the S3 config import integration. | No | `true` +| `spring.cloud.aws.s3.config.reload.strategy` | `Enum` | `refresh` | The strategy to use when firing a reload (`refresh`, `restart_context`) +| `spring.cloud.aws.s3.config.reload.period` | `Duration`| `15s` | The period for verifying changes +| `spring.cloud.aws.s3.config.reload.max-wait-time-for-restart` | `Duration`| `2s` | The maximum time between the detection of changes in property source and the application context restart when `restart_context` strategy is used. +|=== === IAM Permissions Following IAM permissions are required by Spring Cloud AWS: -[cols="2,1"] -|=== -| Downloading files | `s3:GetObject` -| Searching files | `s3:ListObjects` -| Uploading files | `s3:PutObject` +[cols="2,3,1,1"] |=== +| Name | Description | Required | Default value +| `spring.cloud.aws.s3.vector.enabled` | Enables the S3VectorsClient autoconfiguration. | No | `true` +| `spring.cloud.aws.s3.vector.endpoint` | Configures endpoint used by `S3VectorsClient`. | No | `http://localhost:4566` +| `spring.cloud.aws.s3.vector.region` | Configures region used by `S3VectorsClient`. | No | `eu-west-1` + + + + +=== Example of IAM policy for Spring Cloud AWS demo bucket Sample IAM policy granting access to `spring-cloud-aws-demo` bucket: diff --git a/spring-cloud-aws-autoconfigure/pom.xml b/spring-cloud-aws-autoconfigure/pom.xml index 81dfca2b2..d0180b1f3 100644 --- a/spring-cloud-aws-autoconfigure/pom.xml +++ b/spring-cloud-aws-autoconfigure/pom.xml @@ -176,6 +176,12 @@ amazon-s3-encryption-client-java true + + + software.amazon.awssdk + s3vectors + true + io.micrometer micrometer-observation-test diff --git a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3VectorClientAutoConfiguration.java b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3VectorClientAutoConfiguration.java new file mode 100644 index 000000000..035de8f98 --- /dev/null +++ b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3VectorClientAutoConfiguration.java @@ -0,0 +1,51 @@ +package io.awspring.cloud.autoconfigure.s3; + +import io.awspring.cloud.autoconfigure.AwsSyncClientCustomizer; +import io.awspring.cloud.autoconfigure.core.*; +import io.awspring.cloud.autoconfigure.s3.properties.S3VectorProperties; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import software.amazon.awssdk.services.s3vectors.S3VectorsClient; +import software.amazon.awssdk.services.s3vectors.S3VectorsClientBuilder; + +/** + * @author Matej Nedic + * @since 3.5.0 + */ +@AutoConfiguration +@ConditionalOnClass({S3VectorsClient.class}) +@EnableConfigurationProperties({S3VectorProperties.class, AwsProperties.class}) +@AutoConfigureAfter({ CredentialsProviderAutoConfiguration.class, RegionProviderAutoConfiguration.class }) +@ConditionalOnProperty(name = "spring.cloud.aws.s3.vector.enabled", havingValue = "true", matchIfMissing = true) +public class S3VectorClientAutoConfiguration { + + private final S3VectorProperties properties; + + public S3VectorClientAutoConfiguration(S3VectorProperties properties) { + this.properties = properties; + } + + @Bean + @ConditionalOnMissingBean + S3VectorsClientBuilder s3VectorsClientBuilder(AwsClientBuilderConfigurer awsClientBuilderConfigurer, + ObjectProvider> configurer, + ObjectProvider connectionDetails, + ObjectProvider s3ClientCustomizers, + ObjectProvider awsSyncClientCustomizers) { + + return awsClientBuilderConfigurer.configureSyncClient(S3VectorsClient.builder(), this.properties, + connectionDetails.getIfAvailable(), configurer.getIfAvailable(), s3ClientCustomizers.orderedStream(), + awsSyncClientCustomizers.orderedStream()); + } + + @Bean + S3VectorsClient s3VectorsClient(S3VectorsClientBuilder builder) { + return builder.build(); + } +} diff --git a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3VectorClientCustomizer.java b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3VectorClientCustomizer.java new file mode 100644 index 000000000..a132ca901 --- /dev/null +++ b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/S3VectorClientCustomizer.java @@ -0,0 +1,15 @@ +package io.awspring.cloud.autoconfigure.s3; + +import io.awspring.cloud.autoconfigure.AwsClientCustomizer; +import software.amazon.awssdk.services.s3.S3ClientBuilder; +import software.amazon.awssdk.services.s3vectors.S3VectorsClientBuilder; + +/** + * Callback interface that can be used to customize a {@link S3ClientBuilder}. + * + * @author Matej Nedic + * @since 3.5.0 + */ +@FunctionalInterface +public interface S3VectorClientCustomizer extends AwsClientCustomizer { +} diff --git a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/properties/S3VectorProperties.java b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/properties/S3VectorProperties.java new file mode 100644 index 000000000..6303b662e --- /dev/null +++ b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/s3/properties/S3VectorProperties.java @@ -0,0 +1,19 @@ +package io.awspring.cloud.autoconfigure.s3.properties; + +import io.awspring.cloud.autoconfigure.AwsClientProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; + + +/** + * @author Matej Nedic + */ +@ConfigurationProperties(prefix = S3VectorProperties.PREFIX) +public class S3VectorProperties extends AwsClientProperties { + + /** + * The prefix used for S3 related properties. + */ + public static final String PREFIX = "spring.cloud.aws.s3.vector"; + + +} diff --git a/spring-cloud-aws-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-cloud-aws-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 657cbf245..9e5b9c476 100644 --- a/spring-cloud-aws-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-cloud-aws-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -7,6 +7,7 @@ io.awspring.cloud.autoconfigure.ses.SesAutoConfiguration io.awspring.cloud.autoconfigure.s3.S3TransferManagerAutoConfiguration io.awspring.cloud.autoconfigure.s3.S3AutoConfiguration io.awspring.cloud.autoconfigure.s3.S3CrtAsyncClientAutoConfiguration +io.awspring.cloud.autoconfigure.s3.S3VectorClientAutoConfiguration io.awspring.cloud.autoconfigure.sns.SnsAutoConfiguration io.awspring.cloud.autoconfigure.sqs.SqsAutoConfiguration io.awspring.cloud.autoconfigure.dynamodb.DynamoDbAutoConfiguration diff --git a/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/S3VectorAutoConfigurationTests.java b/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/S3VectorAutoConfigurationTests.java new file mode 100644 index 000000000..306d765d8 --- /dev/null +++ b/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/S3VectorAutoConfigurationTests.java @@ -0,0 +1,39 @@ +package io.awspring.cloud.autoconfigure.s3; + +import io.awspring.cloud.autoconfigure.core.AwsAutoConfiguration; +import io.awspring.cloud.autoconfigure.core.CredentialsProviderAutoConfiguration; +import io.awspring.cloud.autoconfigure.core.RegionProviderAutoConfiguration; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import software.amazon.awssdk.services.s3vectors.S3VectorsClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for {@link S3AutoConfiguration} class + * + * @author Matej Nedic + */ +public class S3VectorAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("spring.cloud.aws.region.static:eu-west-1") + .withConfiguration(AutoConfigurations.of(AwsAutoConfiguration.class, RegionProviderAutoConfiguration.class, + CredentialsProviderAutoConfiguration.class, S3VectorClientAutoConfiguration.class)); + + + @Test + void createsS3VectorClientBean() { + this.contextRunner.run(context -> { + assertThat(context).hasSingleBean(S3VectorsClient.class); + }); + } + + @Test + void s3VectorClientAutoConfigurationIsDisabled() { + this.contextRunner.withPropertyValues("spring.cloud.aws.s3.vector.enabled:false").run(context -> { + assertThat(context).doesNotHaveBean(S3VectorsClient.class); + }); + } +} diff --git a/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/S3VectorClientCustomizerTests.java b/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/S3VectorClientCustomizerTests.java new file mode 100644 index 000000000..3ec752f68 --- /dev/null +++ b/spring-cloud-aws-autoconfigure/src/test/java/io/awspring/cloud/autoconfigure/s3/S3VectorClientCustomizerTests.java @@ -0,0 +1,112 @@ +package io.awspring.cloud.autoconfigure.s3; + +import io.awspring.cloud.autoconfigure.AwsSyncClientCustomizer; +import io.awspring.cloud.autoconfigure.ConfiguredAwsClient; +import io.awspring.cloud.autoconfigure.core.AwsAutoConfiguration; +import io.awspring.cloud.autoconfigure.core.CredentialsProviderAutoConfiguration; +import io.awspring.cloud.autoconfigure.core.RegionProviderAutoConfiguration; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import software.amazon.awssdk.http.apache.ApacheHttpClient; +import software.amazon.awssdk.services.s3vectors.S3VectorsClient; + +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Tests for {@link S3VectorClientCustomizer}. + * + * @author Matej Nedic + * @author Maciej Walkowiak + */ +public class S3VectorClientCustomizerTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("spring.cloud.aws.region.static:eu-west-1", + "spring.cloud.aws.credentials.access-key:noop", "spring.cloud.aws.credentials.secret-key:noop") + .withConfiguration(AutoConfigurations.of(AwsAutoConfiguration.class, RegionProviderAutoConfiguration.class, + CredentialsProviderAutoConfiguration.class, S3VectorClientAutoConfiguration.class)); + + @Test + void customClientCustomizer() { + contextRunner.withUserConfiguration(S3VectorClientCustomizerTests.CustomizerConfig.class).run(context -> { + ConfiguredAwsClient client = new ConfiguredAwsClient(context.getBean(S3VectorsClient.class)); + assertThat(client.getApiCallTimeout()).describedAs("sets property from first customizer") + .isEqualTo(Duration.ofMillis(2001)); + assertThat(client.getApiCallAttemptTimeout()).describedAs("sets property from second customizer") + .isEqualTo(Duration.ofMillis(2002)); + assertThat(client.getSyncHttpClient()).describedAs("sets property from common client customizer") + .isNotNull(); + }); + } + + @Test + void customClientCustomizerWithOrder() { + contextRunner.withUserConfiguration(S3VectorClientCustomizerTests.CustomizerConfigWithOrder.class).run(context -> { + ConfiguredAwsClient client = new ConfiguredAwsClient(context.getBean(S3VectorsClient.class)); + assertThat(client.getApiCallTimeout()) + .describedAs("property from the customizer with higher order takes precedence") + .isEqualTo(Duration.ofMillis(2001)); + }); + } + + + @Configuration(proxyBeanMethods = false) + static class CustomizerConfig { + + @Bean + S3VectorClientCustomizer customizer() { + return builder -> { + builder.overrideConfiguration(builder.overrideConfiguration().copy(c -> { + c.apiCallTimeout(Duration.ofMillis(2001)); + })); + }; + } + + @Bean + S3VectorClientCustomizer customizer2() { + return builder -> { + builder.overrideConfiguration(builder.overrideConfiguration().copy(c -> { + c.apiCallAttemptTimeout(Duration.ofMillis(2002)); + })); + }; + } + + @Bean + AwsSyncClientCustomizer awsSyncClientCustomizer() { + return builder -> { + builder.httpClient(ApacheHttpClient.builder().connectionTimeout(Duration.ofMillis(1542)).build()); + }; + } + } + + @Configuration(proxyBeanMethods = false) + static class CustomizerConfigWithOrder { + + @Bean + @Order(2) + S3VectorClientCustomizer customizer() { + return builder -> { + builder.overrideConfiguration(builder.overrideConfiguration().copy(c -> { + c.apiCallTimeout(Duration.ofMillis(2001)); + })); + }; + } + + @Bean + @Order(1) + S3VectorClientCustomizer customizer2() { + return builder -> { + builder.overrideConfiguration(builder.overrideConfiguration().copy(c -> { + c.apiCallTimeout(Duration.ofMillis(2000)); + })); + }; + } + } +} diff --git a/spring-cloud-aws-dependencies/pom.xml b/spring-cloud-aws-dependencies/pom.xml index 3f37598a4..8a2cf1a3f 100644 --- a/spring-cloud-aws-dependencies/pom.xml +++ b/spring-cloud-aws-dependencies/pom.xml @@ -24,7 +24,7 @@ 2.31.0 - 2.31.54 + 2.32.4 2.0.5 3.3.5 1.6