Skip to content

Commit

Permalink
Merge pull request #6 from Geeky-Hacker/localstack
Browse files Browse the repository at this point in the history
Add LocalStack
  • Loading branch information
kasramp authored Sep 14, 2024
2 parents 0d26b5e + 7304166 commit b37075d
Show file tree
Hide file tree
Showing 13 changed files with 286 additions and 3 deletions.
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

0 comments on commit b37075d

Please sign in to comment.