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

Auto create AWS clients for LocalStack Testcontainers #1274

Merged
merged 1 commit into from
Dec 3, 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
22 changes: 22 additions & 0 deletions connectors/citrus-testcontainers/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,28 @@
<artifactId>commons-dbcp2</artifactId>
</dependency>

<!-- Optional AWS clients -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>sqs</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>sns</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>dynamodb</artifactId>
<scope>provided</scope>
</dependency>

<!-- Optional Quarkus Test integration -->
<dependency>
<groupId>io.quarkus</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 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
*
* http://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 org.citrusframework.testcontainers.aws2;

import java.util.Optional;

import org.citrusframework.spi.ReferenceResolver;
import org.citrusframework.testcontainers.aws2.client.DefaultClientFactoryFinder;

@FunctionalInterface
public interface ClientFactory<T> {

/**
* Create client for given LocalStackContainer.
* @param container
* @return
*/
T createClient(LocalStackContainer container);

/**
* Checks if created client is suitable for given service.
* @param service
* @return
*/
default boolean supports(LocalStackContainer.Service service) {
return service != null;
}

static Optional<ClientFactory<?>> lookup(ReferenceResolver referenceResolver, LocalStackContainer.Service service) {
Optional<ClientFactory<?>> clientFactoryBean = referenceResolver.resolveAll(ClientFactory.class).values()
.stream()
.filter(factory -> factory.supports(service))
.findFirst()
.map(factory -> (ClientFactory<?>) factory);

if (clientFactoryBean.isPresent()) {
return clientFactoryBean;
}

return lookup(service);
}

static Optional<ClientFactory<?>> lookup(LocalStackContainer.Service service) {
return new DefaultClientFactoryFinder().find(service);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;
Expand All @@ -45,6 +48,8 @@ public class LocalStackContainer extends GenericContainer<LocalStackContainer> {
private static final String DOCKER_IMAGE_TAG = LocalStackSettings.getVersion();

private final Set<Service> services = new HashSet<>();
private final Map<Service, Object> clients = new HashMap<>();

private String secretKey = "secretkey";
private String accessKey = "accesskey";
private String region = Region.US_EAST_1.id();
Expand Down Expand Up @@ -183,6 +188,34 @@ public Service[] getServices() {
return services.toArray(Service[]::new);
}

public void addClient(Service service, Object client) {
this.clients.put(service, client);
}

public <T> T getClient(Service service) {
if (!services.contains(service)) {
throw new CitrusRuntimeException("Unable to create client for disabled service: %s".formatted(service));
}

Object client = clients.get(service);
if (client != null) {
return (T) client;
}

// lazy load client for this container
Optional<ClientFactory<?>> clientFactory = ClientFactory.lookup(service);
if (clientFactory.isPresent()) {
client = clientFactory.get().createClient(this);
clients.put(service, client);
}

if (client != null) {
return (T) client;
}

throw new CitrusRuntimeException("Missing client for service %s".formatted(service));
}

public enum Service {
CLOUD_WATCH("cloudwatch"),
DYNAMODB("dynamodb"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ public class LocalStackSettings {
private static final String CONTAINER_NAME_ENV = LOCALSTACK_ENV_PREFIX + "CONTAINER_NAME";
public static final String CONTAINER_NAME_DEFAULT = "aws2Container";

private static final String AUTO_CREATE_CLIENTS_PROPERTY = LOCALSTACK_PROPERTY_PREFIX + "auto.create.clients";
private static final String AUTO_CREATE_CLIENTS_ENV = LOCALSTACK_ENV_PREFIX + "AUTO_CREATE_CLIENTS";
public static final String AUTO_CREATE_CLIENTS_DEFAULT = "true";

private static final String STARTUP_TIMEOUT_PROPERTY = LOCALSTACK_PROPERTY_PREFIX + "startup.timeout";
private static final String STARTUP_TIMEOUT_ENV = LOCALSTACK_ENV_PREFIX + "STARTUP_TIMEOUT";

Expand Down Expand Up @@ -100,6 +104,15 @@ public static String getContainerName() {
System.getenv(CONTAINER_NAME_ENV) != null ? System.getenv(CONTAINER_NAME_ENV) : CONTAINER_NAME_DEFAULT);
}

/**
* Auto create clients for enabled services and add them as beans to the Citrus registry.
* @return the enabled/disabled flag.
*/
public static boolean isAutoCreateClients() {
return Boolean.parseBoolean(System.getProperty(AUTO_CREATE_CLIENTS_PROPERTY,
System.getenv(AUTO_CREATE_CLIENTS_ENV) != null ? System.getenv(AUTO_CREATE_CLIENTS_ENV) : AUTO_CREATE_CLIENTS_DEFAULT));
}

/**
* Time in seconds to wait for the container to startup and accept connections.
* @return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,50 @@

import java.util.Arrays;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;

import org.citrusframework.context.TestContext;
import org.citrusframework.testcontainers.TestContainersSettings;
import org.citrusframework.testcontainers.actions.StartTestcontainersAction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.wait.strategy.Wait;

public class StartLocalStackAction extends StartTestcontainersAction<LocalStackContainer> {

/** Logger */
private static final Logger logger = LoggerFactory.getLogger(StartLocalStackAction.class);

private final boolean autoCreateClients;

public StartLocalStackAction(Builder builder) {
super(builder);
this.autoCreateClients = builder.autoCreateClients;
}

@Override
protected void exposeConnectionSettings(LocalStackContainer container, TestContext context) {
LocalStackSettings.exposeConnectionSettings(container, serviceName, context);

if (autoCreateClients) {
for (LocalStackContainer.Service service : container.getServices()) {
String clientName = "%sClient".formatted(service.getServiceName());
if (context.getReferenceResolver().isResolvable(clientName)) {
// client bean with same name already exists - do not overwrite
continue;
}

Optional<ClientFactory<?>> clientFactory = ClientFactory.lookup(context.getReferenceResolver(), service);
if (clientFactory.isPresent()) {
Object client = clientFactory.get().createClient(container);
container.addClient(service, client);
context.getReferenceResolver().bind(clientName, client);
} else {
logger.warn("Missing client factory for service '%s' - no client created for this service".formatted(service));
}
}
}
}

/**
Expand All @@ -45,6 +73,8 @@ public static class Builder extends AbstractBuilder<LocalStackContainer, StartLo

private final Set<LocalStackContainer.Service> services = new HashSet<>();

private boolean autoCreateClients = LocalStackSettings.isAutoCreateClients();

public Builder() {
withStartupTimeout(LocalStackSettings.getStartupTimeout());
}
Expand All @@ -69,6 +99,11 @@ public Builder withServices(Set<LocalStackContainer.Service> services) {
return this;
}

public Builder autoCreateClients(boolean enabled) {
this.autoCreateClients = enabled;
return this;
}

@Override
protected void prepareBuild() {
if (containerName == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 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
*
* http://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 org.citrusframework.testcontainers.aws2.client;

import java.util.Optional;

import org.citrusframework.testcontainers.aws2.ClientFactory;
import org.citrusframework.testcontainers.aws2.LocalStackContainer;

public class DefaultClientFactoryFinder {

public Optional<ClientFactory<?>> find(LocalStackContainer.Service service) {
return switch (service) {
case S3 -> Optional.of(new S3ClientFactory());
case SQS -> Optional.of(new SqsClientFactory());
case SNS -> Optional.of(new SnsClientFactory());
case DYNAMODB -> Optional.of(new DynamoDbClientFactory());
default -> Optional.empty();
};
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 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
*
* http://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 org.citrusframework.testcontainers.aws2.client;

import org.citrusframework.testcontainers.aws2.ClientFactory;
import org.citrusframework.testcontainers.aws2.LocalStackContainer;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;

public class DynamoDbClientFactory implements ClientFactory<DynamoDbClient> {

@Override
public DynamoDbClient createClient(LocalStackContainer container) {
return DynamoDbClient.builder()
.endpointOverride(container.getServiceEndpoint())
.credentialsProvider(
StaticCredentialsProvider.create(
AwsBasicCredentials.create(container.getAccessKey(), container.getSecretKey())
)
)
.region(Region.of(container.getRegion()))
.build();
}

@Override
public boolean supports(LocalStackContainer.Service service) {
return LocalStackContainer.Service.DYNAMODB == service;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 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
*
* http://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 org.citrusframework.testcontainers.aws2.client;

import org.citrusframework.testcontainers.aws2.ClientFactory;
import org.citrusframework.testcontainers.aws2.LocalStackContainer;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;

public class S3ClientFactory implements ClientFactory<S3Client> {

@Override
public S3Client createClient(LocalStackContainer container) {
return S3Client.builder()
.endpointOverride(container.getServiceEndpoint())
.credentialsProvider(
StaticCredentialsProvider.create(
AwsBasicCredentials.create(container.getAccessKey(), container.getSecretKey())
)
)
.forcePathStyle(true)
.region(Region.of(container.getRegion()))
.build();
}

@Override
public boolean supports(LocalStackContainer.Service service) {
return LocalStackContainer.Service.S3 == service;
}
}
Loading
Loading