Skip to content

Add LocalStack docker-compose file #6

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

Merged
merged 6 commits into from
Sep 14, 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
8 changes: 7 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ jobs:
distribution: 'zulu'
java-version: '21'
- name: Run tests
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: mvn clean verify
- name: Build with Maven
run: mvn -B package --file pom.xml
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: mvn -B package --file pom.xml
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ replay_pid*
target/
**/target/
**/*.iml

# LocalStack volume
spring-boot/volume
23 changes: 23 additions & 0 deletions spring-boot/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
# Related articles

- [How to mock @Value field in Spring Boot](https://www.geekyhacker.com/how-to-mock-at-value-field-in-spring-boot/)


## LocalStack

To run LocalStack in this project:

```bash
$ docker-compose up
```

To use AWS Secrets manager secret with the application, go to the `scripts` directory and run:

```bash
$ ./create_secret.sh
```

That creates a secret for `api.key` property of the application. Without that, the application defaults to `testKey` value and on test to `fakeApiKey` value.

After that you can access the LocalStack environment on `http://localhost:4566`. For example, to get list of secrets:

```bash
$ aws --endpoint-url=http://localhost:4566 --region=eu-central-1 secretsmanager list-secrets
```
15 changes: 15 additions & 0 deletions spring-boot/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
version: "3.8"

services:
localstack:
container_name: "${LOCALSTACK_DOCKER_NAME:-localstack-main}"
image: localstack/localstack
ports:
- "127.0.0.1:4566:4566" # LocalStack Gateway
- "127.0.0.1:4510-4559:4510-4559" # external services port range
environment:
# LocalStack configuration: https://docs.localstack.cloud/references/configuration/
- DEBUG=${DEBUG:-0}
volumes:
- "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack"
- "/var/run/docker.sock:/var/run/docker.sock"
27 changes: 27 additions & 0 deletions spring-boot/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,39 @@
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-starter-secrets-manager</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.20.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>localstack</artifactId>
<version>1.20.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-dependencies</artifactId>
<version>3.1.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
Expand Down
7 changes: 7 additions & 0 deletions spring-boot/scripts/create_secret.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/sh

aws --endpoint-url=http://localhost:4566 --region=eu-central-1 \
secretsmanager create-secret \
--name weather/api/credentials \
--description "Weather API related Credentials" \
--secret-string file://secrets.json
3 changes: 3 additions & 0 deletions spring-boot/scripts/secrets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"api.key": "d3deaf2ba6ab43229be3e58ba2ada8d6"
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,19 @@ public class WeatherApi {
@Value("${api.endpoint}")
private String apiEndpoint;

@Value("${api.key}")
private String apiKey;

public double getCurrentTemperature() {
logger.info("Getting the current weather temperature from {} endpoint", apiEndpoint);
logger.info("Getting the current weather temperature from {} endpoint with key {}", apiEndpoint, apiKey);
return -1;
}

public String getApiEndpoint() {
return apiEndpoint;
}

public String getApiKey() {
return apiKey;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.geekyhacker.springboot.store;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.awssdk.services.secretsmanager.model.*;

import java.util.List;
import java.util.Map;

@Service
public class SecretStore {

private final SecretsManagerClient secretsManagerClient;
private final ObjectMapper objectMapper;

public SecretStore(SecretsManagerClient secretsManagerClient, ObjectMapper objectMapper) {
this.secretsManagerClient = secretsManagerClient;
this.objectMapper = objectMapper;
}

public Map<String, String> getSecretAsMap(String secretName) throws JsonProcessingException {
return objectMapper.readValue(getSecretValue(secretName), new TypeReference<>() {
});
}

public String getSecretValue(String secretName) {
GetSecretValueResponse secretValueResponse = secretsManagerClient.getSecretValue(GetSecretValueRequest.builder().secretId(secretName).build());
return secretValueResponse.secretString();
}

public List<String> listSecrets() {
return secretsManagerClient.listSecrets().secretList().stream().map(SecretListEntry::name).toList();
}

public void createSecret(String secretName, String secretValue) {
CreateSecretRequest createSecretRequest = CreateSecretRequest.builder()
.name(secretName)
.secretString(secretValue)
.build();
secretsManagerClient.createSecret(createSecretRequest);
}

public void deleteSecret(String secretName, boolean forceDelete) {
secretsManagerClient.deleteSecret(DeleteSecretRequest.builder().secretId(secretName).forceDeleteWithoutRecovery(forceDelete).build());
}

public void deleteAllSecrets() {
listSecrets().forEach(secretName -> deleteSecret(secretName, true));
}

public void updateSecret(String secretName, String secretValue) {
secretsManagerClient.updateSecret(UpdateSecretRequest.builder().secretId(secretName).secretString(secretValue).build());
}
}
4 changes: 4 additions & 0 deletions spring-boot/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
spring.application.name=geeky-hacker-spring-boot
spring.cloud.aws.secretsmanager.region=eu-central-1
spring.cloud.aws.secretsmanager.endpoint=http://localhost:4566
spring.config.import=optional:aws-secretsmanager:weather/api/credentials
api.endpoint=https://eris.madadipouya.com/v1/weather/currentbyip
api.key=testKey
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,11 @@ void testGetWeatherApiEndpointAddress() {

assertEquals("https://dummyjson.com/test", result);
}

@Test
void testGetApiKey() {
String apiKey = weatherApi.getApiKey();

assertEquals("fakeApiKey", apiKey);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package com.geekyhacker.springboot.store;

import com.fasterxml.jackson.core.JsonProcessingException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.testcontainers.containers.localstack.LocalStackContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;

@Testcontainers
@SpringBootTest
class SecretStoreLocalStackIntegrationTest {

private static final String WEATHER_SECRET_NAME = "weather/api/credentials";
private static final String SIMPLE_SECRET_NAME = "mySecret";
private static final String SIMPLE_SECRET_VALUE = "mySecretValue";
private static final String WEATHER_SECRET_KEY = "api.key.test";
private static final String WEATHER_SECRET_KEY_VALUE = """
{
"api.key.test": "testApiKey"
}
""";

@Container
private static final LocalStackContainer localStackContainer = new LocalStackContainer(DockerImageName.parse("localstack/localstack"))
.withServices(LocalStackContainer.Service.SECRETSMANAGER)
.withEnv("LOCALSTACK_HOSTNAME", "localhost")
.withEnv("HOSTNAME", "localhost");

static {
localStackContainer.setPortBindings(List.of("4566:4566"));
}

@Autowired
private SecretStore secretStore;

@BeforeEach
void setSecretsManagerState() {
secretStore.deleteSecret(WEATHER_SECRET_NAME, true);
secretStore.deleteSecret(SIMPLE_SECRET_NAME, true);
}

@Test
void testGetSecretAsMap() throws JsonProcessingException {
secretStore.createSecret(WEATHER_SECRET_NAME, WEATHER_SECRET_KEY_VALUE);

Map<String, String> secretKeyValue = secretStore.getSecretAsMap(WEATHER_SECRET_NAME);

assertEquals(WEATHER_SECRET_KEY, new ArrayList<>(secretKeyValue.keySet()).getFirst());
assertEquals("testApiKey", secretKeyValue.get(WEATHER_SECRET_KEY));
}

@Test
void testGetSecret() {
secretStore.createSecret(WEATHER_SECRET_NAME, WEATHER_SECRET_KEY_VALUE);

String secretValue = secretStore.getSecretValue(WEATHER_SECRET_NAME);

assertEquals(WEATHER_SECRET_KEY_VALUE, secretValue);
}

@Test
void testGetSecretsList() {
secretStore.createSecret(WEATHER_SECRET_NAME, WEATHER_SECRET_KEY_VALUE);

List<String> secretNames = secretStore.listSecrets();

assertEquals(1, secretNames.size());
assertEquals(WEATHER_SECRET_NAME, secretNames.getFirst());
}

@Test
void testCreateSecret() {
secretStore.createSecret("mySecret", "mySecretValue");

String secretValue = secretStore.getSecretValue("mySecret");
assertEquals("mySecretValue", secretValue);
}

@Test
void testDeleteSecret() {
secretStore.createSecret(WEATHER_SECRET_NAME, WEATHER_SECRET_KEY_VALUE);
secretStore.createSecret(SIMPLE_SECRET_NAME, SIMPLE_SECRET_VALUE);

secretStore.deleteSecret(SIMPLE_SECRET_NAME, true);

assertFalse(secretStore.listSecrets().contains(SIMPLE_SECRET_NAME));
assertTrue(secretStore.listSecrets().contains(WEATHER_SECRET_NAME));
}

@Test
void testDeleteAllSecrets() {
secretStore.createSecret(WEATHER_SECRET_NAME, WEATHER_SECRET_KEY_VALUE);
secretStore.createSecret(SIMPLE_SECRET_NAME, SIMPLE_SECRET_VALUE);

secretStore.deleteAllSecrets();

assertTrue(secretStore.listSecrets().isEmpty());
}

@Test
void testUpdateSecret() {
secretStore.createSecret(SIMPLE_SECRET_NAME, SIMPLE_SECRET_VALUE);

assertEquals(SIMPLE_SECRET_VALUE, secretStore.getSecretValue(SIMPLE_SECRET_NAME));

secretStore.updateSecret(SIMPLE_SECRET_NAME, "newValue");

assertEquals("newValue", secretStore.getSecretValue(SIMPLE_SECRET_NAME));
}
}
6 changes: 5 additions & 1 deletion spring-boot/src/test/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
api.endpoint=https://dummyjson.com/test
spring.cloud.aws.secretsmanager.region=eu-central-1
spring.cloud.aws.secretsmanager.endpoint=http://localhost:4566
spring.config.import=optional:aws-secretsmanager:weather/api/credentials
api.endpoint=https://dummyjson.com/test
api.key=fakeApiKey