Skip to content

Commit

Permalink
Add localstack docker compose support (#1123)
Browse files Browse the repository at this point in the history
Co-authored-by: Dominik Kovacs <dominik.kovacs.ext@uniqa.at>
  • Loading branch information
dominik-kovacs and Dominik Kovacs authored Sep 14, 2024
1 parent 4fceb0f commit e3530a8
Show file tree
Hide file tree
Showing 17 changed files with 434 additions and 0 deletions.
34 changes: 34 additions & 0 deletions docs/src/main/asciidoc/docker-compose.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[#spring-cloud-aws-docker-compose]
== Docker Compose

Spring Cloud AWS provides Docker Compose support for https://docs.localstack.cloud/references/docker-images/[LocalStack docker images] which simplifies local development of 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-docker-compose</artifactId>
</dependency>
----

For more information about Spring Docker Compose support please refer to https://docs.spring.io/spring-boot/reference/features/docker-compose.html[official Spring documentation]

=== Example docker-compose.yaml file

[source,yaml]
----
services:
localstack:
image: localstack/localstack
environment:
AWS_ACCESS_KEY_ID: noop
AWS_SECRET_ACCESS_KEY: noop
AWS_DEFAULT_REGION: eu-central-1
ports:
- "4566:4566"
----

`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_DEFAULT_REGION` are required environment variables to ensure proper integration.

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

include::testing.adoc[]

include::docker-compose.adoc[]

include::migration.adoc[]

== Configuration properties
Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<module>spring-cloud-aws-core</module>
<module>spring-cloud-aws-autoconfigure</module>
<module>spring-cloud-aws-dependencies</module>
<module>spring-cloud-aws-docker-compose</module>
<module>spring-cloud-aws-parameter-store</module>
<module>spring-cloud-aws-secrets-manager</module>
<module>spring-cloud-aws-ses</module>
Expand Down
6 changes: 6 additions & 0 deletions spring-cloud-aws-dependencies/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-docker-compose</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-starter</artifactId>
Expand Down
33 changes: 33 additions & 0 deletions spring-cloud-aws-docker-compose/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-aws</artifactId>
<groupId>io.awspring.cloud</groupId>
<version>3.2.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>spring-cloud-aws-docker-compose</artifactId>
<name>Spring Cloud AWS Docker Compose</name>
<description>Spring Cloud AWS Docker Compose Integration</description>

<dependencies>
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-docker-compose</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* 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.docker.compose;

import io.awspring.cloud.autoconfigure.core.AwsConnectionDetails;
import java.net.URI;
import org.springframework.boot.docker.compose.core.RunningService;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;

/**
* {@link DockerComposeConnectionDetailsFactory} to create {@link AwsConnectionDetails} for a {@code localstack}
* service.
*
* @author Dominik Kovács
* @since 3.2.0
*/
class AwsDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory<AwsConnectionDetails> {

private static final String[] LOCALSTACK_CONTAINER_NAMES = { "localstack/localstack", "localstack/localstack-pro" };

private static final int LOCALSTACK_PORT = 4566;

AwsDockerComposeConnectionDetailsFactory() {
super(LOCALSTACK_CONTAINER_NAMES);
}

@Override
protected AwsConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) {
return new AwsDockerComposeConnectionDetails(source.getRunningService());
}

/**
* {@link DockerComposeConnectionDetails} backed by a {@code localstack} {@link RunningService}.
*/
private static final class AwsDockerComposeConnectionDetails extends DockerComposeConnectionDetails
implements AwsConnectionDetails {

private final LocalStackEnvironment environment;

private final URI endpoint;

private AwsDockerComposeConnectionDetails(RunningService service) {
super(service);
this.environment = new LocalStackEnvironment(service.env());
this.endpoint = URI.create("http://%s:%s".formatted(service.host(), service.ports().get(LOCALSTACK_PORT)));
}

@Override
public URI getEndpoint() {
return this.endpoint;
}

@Override
public String getRegion() {
return this.environment.getRegion();
}

@Override
public String getAccessKey() {
return this.environment.getAccessKey();
}

@Override
public String getSecretKey() {
return this.environment.getSecretKey();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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.docker.compose;

import java.util.Map;

/**
* LocalStack environment details.
*
* @author Dominik Kovács
* @since 3.2.0
*/
class LocalStackEnvironment {

private final String region;

private final String accessKey;

private final String secretKey;

LocalStackEnvironment(Map<String, String> env) {
this.region = env.get("AWS_DEFAULT_REGION");
this.accessKey = env.get("AWS_ACCESS_KEY_ID");
this.secretKey = env.get("AWS_SECRET_ACCESS_KEY");
}

String getRegion() {
return this.region;
}

String getAccessKey() {
return this.accessKey;
}

String getSecretKey() {
return this.secretKey;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Docker Compose integration.
*/
@org.springframework.lang.NonNullApi
@org.springframework.lang.NonNullFields
package io.awspring.cloud.docker.compose;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\
io.awspring.cloud.docker.compose.AwsDockerComposeConnectionDetailsFactory
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.awspring.cloud.docker.compose;

import io.awspring.cloud.autoconfigure.core.AwsConnectionDetails;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;

import java.io.IOException;
import java.net.URI;
import java.util.LinkedHashMap;

import static org.assertj.core.api.Assertions.assertThat;

class AwsDockerComposeConnectionDetailsFactoryTest {

private final Resource dockerComposeResource = new ClassPathResource("docker-compose.yaml");

@AfterAll
static void shutDown() {
var shutdownHandlers = SpringApplication.getShutdownHandlers();
((Runnable) shutdownHandlers).run();
}

@Test
void runCreatesConnectionDetailsThatCanAccessLocalStack() throws IOException {
var application = new SpringApplication(Config.class);
var properties = new LinkedHashMap<String, Object>();
properties.put("spring.docker.compose.skip.in-tests", "false");
properties.put("spring.docker.compose.file", dockerComposeResource.getFile());
properties.put("spring.docker.compose.stop.command", "down");
application.setDefaultProperties(properties);
var connectionDetails = application.run().getBean(AwsConnectionDetails.class);

assertThat(connectionDetails.getAccessKey()).isEqualTo("noop");
assertThat(connectionDetails.getSecretKey()).isEqualTo("noop");
assertThat(connectionDetails.getRegion()).isEqualTo("eu-central-1");
assertThat(connectionDetails.getEndpoint()).satisfiesAnyOf(
endpoint -> assertThat(endpoint).isEqualTo(URI.create("http://localhost:4566")),
endpoint -> assertThat(endpoint).isEqualTo(URI.create("http://127.0.0.1:4566")));
}

@Configuration(proxyBeanMethods = false)
static class Config {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* 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.docker.compose;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

import java.util.Collections;
import java.util.Map;
import org.junit.jupiter.api.Test;

class LocalStackEnvironmentTest {

@Test
void getRegionWhenRegionIsNotSet() {
var environment = new LocalStackEnvironment(Collections.emptyMap());
assertThat(environment.getRegion()).isNull();
}

@Test
void getRegionWhenRegionIsSet() {
var environment = new LocalStackEnvironment(Map.of("AWS_DEFAULT_REGION", "us-west-1"));
assertThat(environment.getRegion()).isEqualTo("us-west-1");
}

@Test
void getAccessKeyWhenAccessKeyIsNotSet() {
var environment = new LocalStackEnvironment(Collections.emptyMap());
assertThat(environment.getAccessKey()).isNull();
}

@Test
void getAccessKeyWhenAccessKeyIsSet() {
var environment = new LocalStackEnvironment(Map.of("AWS_ACCESS_KEY_ID", "access-key"));
assertThat(environment.getAccessKey()).isEqualTo("access-key");
}

@Test
void getSecretKeyWhenSecretKeyIsNotSet() {
var environment = new LocalStackEnvironment(Collections.emptyMap());
assertThat(environment.getSecretKey()).isNull();
}

@Test
void getSecretKeyWhenSecretKeyIsSet() {
var environment = new LocalStackEnvironment(Map.of("AWS_SECRET_ACCESS_KEY", "secret-key"));
assertThat(environment.getSecretKey()).isEqualTo("secret-key");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
services:
localstack:
image: localstack/localstack
environment:
AWS_ACCESS_KEY_ID: noop
AWS_SECRET_ACCESS_KEY: noop
AWS_DEFAULT_REGION: eu-central-1
ports:
- "4566:4566"
1 change: 1 addition & 0 deletions spring-cloud-aws-samples/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

<modules>
<module>infrastructure</module>
<module>spring-cloud-aws-docker-compose-sample</module>
<module>spring-cloud-aws-dynamodb-sample</module>
<module>spring-cloud-aws-parameter-store-sample</module>
<module>spring-cloud-aws-s3-sample</module>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
services:
localstack:
image: localstack/localstack
container_name: localstack
environment:
SERVICES: sqs
AWS_ACCESS_KEY_ID: noop
AWS_SECRET_ACCESS_KEY: noop
AWS_DEFAULT_REGION: eu-central-1
ports:
- "4566:4566"
Loading

0 comments on commit e3530a8

Please sign in to comment.