Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add @ServiceConnection support. #1075

Merged
merged 7 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/src/main/asciidoc/_configprops.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,4 @@
|spring.cloud.aws.sqs.listener.poll-timeout | | The maximum amount of time for a poll to SQS.
|spring.cloud.aws.sqs.region | | Overrides the default region.

|===
|===
2 changes: 2 additions & 0 deletions docs/src/main/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ include::cloudwatch.adoc[]

include::spring-modulith.adoc[]

include::testing.adoc[]

include::migration.adoc[]

== Configuration properties
Expand Down
27 changes: 27 additions & 0 deletions docs/src/main/asciidoc/testing.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[#testing]
== Testing

Spring Cloud AWS provides https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.testing.testcontainers.service-connections[@ServiceConnection] for https://java.testcontainers.org/modules/localstack/[LocalStack Container] that simplifies using Testcontainers LocalStack module with Spring Cloud AWS based projects.

Maven coordinates, using <<index.adoc#bill-of-materials, Spring Cloud AWS BOM>>:

[source,xml]
----
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-testcontainers</artifactId>
</dependency>
----

Once Spring Cloud AWS detects in application test code a `LocalStackContainer` bean annotated with `@ServiceConnection`, it will automatically configure `region` and `credentials` to point to a LocalStack container.

[source,java]
----
@Bean
@ServiceConnection
LocalStackContainer localStackContainer() {
return new LocalStackContainer(DockerImageName.parse("localstack/localstack:<version>"));
}
----

To understand in depth how service connection works, follow https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.testing.testcontainers.service-connections[Spring Boot Reference Guide] on this topic.
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
<module>spring-cloud-aws-sqs</module>
<module>spring-cloud-aws-dynamodb</module>
<module>spring-cloud-aws-s3</module>
<module>spring-cloud-aws-testcontainers</module>
<module>spring-cloud-aws-starters/spring-cloud-aws-starter</module>
<module>spring-cloud-aws-starters/spring-cloud-aws-starter-dynamodb</module>
<module>spring-cloud-aws-starters/spring-cloud-aws-starter-metrics</module>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,25 @@ public class AwsClientBuilderConfigurer {
}

public <T extends AwsClientBuilder<?, ?>> T configure(T builder) {
return configure(builder, null, null);
return configure(builder, null, null, null);
}

public <T extends AwsClientBuilder<?, ?>> T configure(T builder, @Nullable AwsClientProperties clientProperties,
@Nullable AwsClientCustomizer<T> customizer) {
return configure(builder, clientProperties, null, customizer);
}

public <T extends AwsClientBuilder<?, ?>> T configure(T builder, @Nullable AwsClientProperties clientProperties,
@Nullable AwsConnectionDetails connectionDetails, @Nullable AwsClientCustomizer<T> customizer) {
Assert.notNull(builder, "builder is required");

builder.credentialsProvider(this.credentialsProvider).region(resolveRegion(clientProperties))
builder.credentialsProvider(this.credentialsProvider).region(resolveRegion(clientProperties, connectionDetails))
.overrideConfiguration(this.clientOverrideConfiguration);
Optional.ofNullable(this.awsProperties.getEndpoint()).ifPresent(builder::endpointOverride);
Optional.ofNullable(clientProperties).map(AwsClientProperties::getEndpoint)
.ifPresent(builder::endpointOverride);
Optional.ofNullable(connectionDetails).map(AwsConnectionDetails::getEndpoint)
.ifPresent(builder::endpointOverride);

Optional.ofNullable(this.awsProperties.getDefaultsMode()).ifPresent(builder::defaultsMode);
Optional.ofNullable(this.awsProperties.getFipsEnabled()).ifPresent(builder::fipsEnabled);
Expand All @@ -70,14 +77,21 @@ public class AwsClientBuilderConfigurer {
return builder;
}

public Region resolveRegion(@Nullable AwsClientProperties clientProperties) {
return resolveRegion(clientProperties, this.regionProvider);
public Region resolveRegion(@Nullable AwsClientProperties clientProperties,
@Nullable AwsConnectionDetails connectionDetails) {
return resolveRegion(clientProperties, connectionDetails, this.regionProvider);
}

public static Region resolveRegion(@Nullable AwsClientProperties clientProperties,
AwsRegionProvider regionProvider) {
return clientProperties != null && StringUtils.hasLength(clientProperties.getRegion())
? Region.of(clientProperties.getRegion())
: regionProvider.getRegion();
@Nullable AwsConnectionDetails connectionDetails, AwsRegionProvider regionProvider) {
if (connectionDetails != null && StringUtils.hasLength(connectionDetails.getRegion())) {
return Region.of(connectionDetails.getRegion());
}

if (clientProperties != null && StringUtils.hasLength(clientProperties.getRegion())) {
return Region.of(clientProperties.getRegion());
}

return regionProvider.getRegion();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 2013-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.awspring.cloud.autoconfigure.core;

import java.net.URI;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
import org.springframework.lang.Nullable;

/**
* Details required to establish a connection to a AWS.
*
* @author Maciej Walkowiak
* @since 3.2.0
*/
public interface AwsConnectionDetails extends ConnectionDetails {

/**
* AWS or Localstack endpoint.
*
* @return the AWS or Localstack endpoint or {@code null}
*/
@Nullable
default URI getEndpoint() {
return null;
}

/**
* AWS or Localstack region.
*
* @return the AWS or Localstack region or {@code null}
*/
@Nullable
default String getRegion() {
return null;
}

/**
* Credentials Access Key.
*
* @return the access key
*/
default String getAccessKey() {
return null;
}

/**
* Credentials Secret Key
*
* @return the secret key
*/
default String getSecretKey() {
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
Expand Down Expand Up @@ -60,21 +61,34 @@ public class CredentialsProviderAutoConfiguration {

private final CredentialsProperties properties;
private final AwsRegionProvider regionProvider;
private final ObjectProvider<AwsConnectionDetails> connectionDetails;

public CredentialsProviderAutoConfiguration(CredentialsProperties properties, AwsRegionProvider regionProvider) {
public CredentialsProviderAutoConfiguration(CredentialsProperties properties, AwsRegionProvider regionProvider,
ObjectProvider<AwsConnectionDetails> connectionDetails) {
this.properties = properties;
this.regionProvider = regionProvider;
this.connectionDetails = connectionDetails;
}

@Bean
public AwsCredentialsProvider credentialsProvider() {
return createCredentialsProvider(this.properties, this.regionProvider);
return createCredentialsProvider(this.properties, this.regionProvider, this.connectionDetails.getIfAvailable());
}

public static AwsCredentialsProvider createCredentialsProvider(CredentialsProperties properties,
AwsRegionProvider regionProvider) {
return createCredentialsProvider(properties, regionProvider, null);
}

public static AwsCredentialsProvider createCredentialsProvider(CredentialsProperties properties,
AwsRegionProvider regionProvider, @Nullable AwsConnectionDetails connectionDetails) {
final List<AwsCredentialsProvider> providers = new ArrayList<>();

if (connectionDetails != null && StringUtils.hasText(connectionDetails.getAccessKey())
&& StringUtils.hasText(connectionDetails.getSecretKey())) {
providers.add(createStaticCredentialsProvider(connectionDetails));
}

if (StringUtils.hasText(properties.getAccessKey()) && StringUtils.hasText(properties.getSecretKey())) {
providers.add(createStaticCredentialsProvider(properties));
}
Expand Down Expand Up @@ -115,6 +129,11 @@ private static StaticCredentialsProvider createStaticCredentialsProvider(Credent
.create(AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey()));
}

private static StaticCredentialsProvider createStaticCredentialsProvider(AwsConnectionDetails connectionDetails) {
return StaticCredentialsProvider
.create(AwsBasicCredentials.create(connectionDetails.getAccessKey(), connectionDetails.getSecretKey()));
}

private static ProfileCredentialsProvider createProfileCredentialProvider(Profile profile) {
ProfileFile profileFile;
if (profile.getPath() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,15 @@

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.CredentialsProviderAutoConfiguration;
import io.awspring.cloud.autoconfigure.core.RegionProviderAutoConfiguration;
import io.awspring.cloud.dynamodb.*;
import io.awspring.cloud.dynamodb.DefaultDynamoDbTableNameResolver;
import io.awspring.cloud.dynamodb.DefaultDynamoDbTableSchemaResolver;
import io.awspring.cloud.dynamodb.DynamoDbOperations;
import io.awspring.cloud.dynamodb.DynamoDbTableNameResolver;
import io.awspring.cloud.dynamodb.DynamoDbTableSchemaResolver;
import io.awspring.cloud.dynamodb.DynamoDbTemplate;
import java.io.IOException;
import java.util.List;
import org.springframework.beans.factory.ObjectProvider;
Expand Down Expand Up @@ -57,6 +63,7 @@
@AutoConfigureAfter({ CredentialsProviderAutoConfiguration.class, RegionProviderAutoConfiguration.class })
@ConditionalOnProperty(name = "spring.cloud.aws.dynamodb.enabled", havingValue = "true", matchIfMissing = true)
public class DynamoDbAutoConfiguration {

@ConditionalOnProperty(name = "spring.cloud.aws.dynamodb.dax.url")
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(name = "software.amazon.dax.ClusterDaxClient")
Expand All @@ -65,7 +72,8 @@ static class DaxDynamoDbClient {
@ConditionalOnMissingBean
@Bean
public DynamoDbClient dynamoDbClient(DynamoDbProperties properties, AwsCredentialsProvider credentialsProvider,
AwsRegionProvider regionProvider) throws IOException {
AwsRegionProvider regionProvider, ObjectProvider<AwsConnectionDetails> connectionDetails)
throws IOException {
DaxProperties daxProperties = properties.getDax();

PropertyMapper propertyMapper = PropertyMapper.get();
Expand All @@ -90,8 +98,9 @@ public DynamoDbClient dynamoDbClient(DynamoDbProperties properties, AwsCredentia
propertyMapper.from(daxProperties.getSkipHostNameVerification()).whenNonNull()
.to(configuration::skipHostNameVerification);

configuration.region(AwsClientBuilderConfigurer.resolveRegion(properties, regionProvider))
.credentialsProvider(credentialsProvider).url(properties.getDax().getUrl());
configuration.region(AwsClientBuilderConfigurer.resolveRegion(properties,
connectionDetails.getIfAvailable(), regionProvider)).credentialsProvider(credentialsProvider)
.url(properties.getDax().getUrl());
return ClusterDaxClient.builder().overrideConfiguration(configuration.build()).build();
}

Expand All @@ -104,9 +113,10 @@ static class StandardDynamoDbClient {
@ConditionalOnMissingBean
@Bean
public DynamoDbClient dynamoDbClient(AwsClientBuilderConfigurer awsClientBuilderConfigurer,
ObjectProvider<AwsClientCustomizer<DynamoDbClientBuilder>> configurer, DynamoDbProperties properties) {
return awsClientBuilderConfigurer
.configure(DynamoDbClient.builder(), properties, configurer.getIfAvailable()).build();
ObjectProvider<AwsClientCustomizer<DynamoDbClientBuilder>> configurer,
ObjectProvider<AwsConnectionDetails> connectionDetails, DynamoDbProperties properties) {
return awsClientBuilderConfigurer.configure(DynamoDbClient.builder(), properties,
connectionDetails.getIfAvailable(), configurer.getIfAvailable()).build();
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

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.CredentialsProviderAutoConfiguration;
import io.awspring.cloud.autoconfigure.core.RegionProviderAutoConfiguration;
import io.micrometer.cloudwatch2.CloudWatchConfig;
Expand Down Expand Up @@ -67,9 +68,10 @@ public CloudWatchMeterRegistry cloudWatchMeterRegistry(CloudWatchConfig config,
@ConditionalOnMissingBean
public CloudWatchAsyncClient cloudWatchAsyncClient(CloudWatchProperties properties,
AwsClientBuilderConfigurer awsClientBuilderConfigurer,
ObjectProvider<AwsClientCustomizer<CloudWatchAsyncClientBuilder>> configurer) {
return awsClientBuilderConfigurer
.configure(CloudWatchAsyncClient.builder(), properties, configurer.getIfAvailable()).build();
ObjectProvider<AwsClientCustomizer<CloudWatchAsyncClientBuilder>> configurer,
ObjectProvider<AwsConnectionDetails> connectionDetails) {
return awsClientBuilderConfigurer.configure(CloudWatchAsyncClient.builder(), properties,
connectionDetails.getIfAvailable(), configurer.getIfAvailable()).build();
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
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.s3.properties.S3Properties;
import io.awspring.cloud.s3.InMemoryBufferingS3OutputStreamProvider;
Expand Down Expand Up @@ -68,9 +69,10 @@ public S3AutoConfiguration(S3Properties properties) {
@Bean
@ConditionalOnMissingBean
S3ClientBuilder s3ClientBuilder(AwsClientBuilderConfigurer awsClientBuilderConfigurer,
ObjectProvider<AwsClientCustomizer<S3ClientBuilder>> configurer) {
ObjectProvider<AwsClientCustomizer<S3ClientBuilder>> configurer,
ObjectProvider<AwsConnectionDetails> connectionDetails) {
S3ClientBuilder builder = awsClientBuilderConfigurer.configure(S3Client.builder(), this.properties,
configurer.getIfAvailable());
connectionDetails.getIfAvailable(), configurer.getIfAvailable());

Optional.ofNullable(this.properties.getCrossRegionEnabled()).ifPresent(builder::crossRegionAccessEnabled);

Expand All @@ -89,10 +91,11 @@ S3Template s3Template(S3Client s3Client, S3OutputStreamProvider s3OutputStreamPr
@Bean
@ConditionalOnMissingBean
S3Presigner s3Presigner(S3Properties properties, AwsProperties awsProperties,
AwsCredentialsProvider credentialsProvider, AwsRegionProvider regionProvider) {
AwsCredentialsProvider credentialsProvider, AwsRegionProvider regionProvider,
ObjectProvider<AwsConnectionDetails> connectionDetails) {
S3Presigner.Builder builder = S3Presigner.builder().serviceConfiguration(properties.toS3Configuration())
.credentialsProvider(credentialsProvider)
.region(AwsClientBuilderConfigurer.resolveRegion(properties, regionProvider));
.credentialsProvider(credentialsProvider).region(AwsClientBuilderConfigurer.resolveRegion(properties,
connectionDetails.getIfAvailable(), regionProvider));

if (properties.getEndpoint() != null) {
builder.endpointOverride(properties.getEndpoint());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
package io.awspring.cloud.autoconfigure.s3;

import io.awspring.cloud.autoconfigure.core.AwsClientBuilderConfigurer;
import io.awspring.cloud.autoconfigure.core.AwsConnectionDetails;
import io.awspring.cloud.autoconfigure.core.AwsProperties;
import io.awspring.cloud.autoconfigure.s3.properties.S3CrtClientProperties;
import io.awspring.cloud.autoconfigure.s3.properties.S3Properties;
import java.util.Optional;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
Expand Down Expand Up @@ -61,9 +63,10 @@ public S3CrtAsyncClientAutoConfiguration(S3Properties properties, AwsProperties

@Bean
@ConditionalOnMissingBean
S3AsyncClient s3AsyncClient(AwsCredentialsProvider credentialsProvider) {
S3CrtAsyncClientBuilder builder = S3AsyncClient.crtBuilder().credentialsProvider(credentialsProvider)
.region(this.awsClientBuilderConfigurer.resolveRegion(this.properties));
S3AsyncClient s3AsyncClient(AwsCredentialsProvider credentialsProvider,
ObjectProvider<AwsConnectionDetails> connectionDetails) {
S3CrtAsyncClientBuilder builder = S3AsyncClient.crtBuilder().credentialsProvider(credentialsProvider).region(
this.awsClientBuilderConfigurer.resolveRegion(this.properties, connectionDetails.getIfAvailable()));
Optional.ofNullable(this.awsProperties.getEndpoint()).ifPresent(builder::endpointOverride);
Optional.ofNullable(this.properties.getEndpoint()).ifPresent(builder::endpointOverride);
Optional.ofNullable(this.properties.getCrossRegionEnabled()).ifPresent(builder::crossRegionAccessEnabled);
Expand Down
Loading
Loading