Skip to content

Commit

Permalink
Merge pull request #193 from esuomi/TIS-884/use_rutebanken_storage
Browse files Browse the repository at this point in the history
TIS-884/AWS S3 support through `rutebanken-helpers/storage`
  • Loading branch information
assadriaz authored Jan 23, 2025
2 parents 74f25f7 + 4bcd368 commit e62d146
Show file tree
Hide file tree
Showing 10 changed files with 283 additions and 27 deletions.
75 changes: 69 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,22 +106,64 @@ docker compose down
docker compose --profile aws down
```

#### Supported Docker Compose profiles

Docker Compose has its own profiles which start up additional supporting services to e.g. make specific feature
development easier. You may include any number of additional profiles when working with Docker Compose by listing
them in the commands with the `--profile {profile name}` argument. Multiple profiles are activated by providing the
same attribute multiple times, for example starting Compose environment with profiles a and b would be
```shell
docker compose --profile a --profile b up
```

The provided profiles for Tiamat development are


| profile | description |
|:--------|---------------------------------------------------------------------------------------------------|
| `aws` | Starts up [LocalStack](https://www.localstack.cloud/) meant for developing AWS specific features. |


See [Docker Compose reference](https://docs.docker.com/compose/reference/) for more details.

See [Supported Docker Compose Profiles](#supported-docker-compose-profiles) for more information on provided profiles.

### 2. Run the Service

#### Available Profiles
#### Available Spring Boot Profiles

> **Note!** You must choose at least one of the options from each category below!
> **Note!** `local` profile must always be included!
##### Storage

| profile | description |
|:------------------|--------------------------------------------------|
| `gcs-blobstore` | GCP GCS implementation of tiamat's blob storage |
| `local-blobstore` | Use local directory as backing storage location. |
| profile | description |
|:-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `gcs-blobstore` | GCP GCS implementation of tiamat's blob storage |
| `local-blobstore` | Use local directory as backing storage location. |
| `rutebanken-blobstore` | Use [`rutebanken-helpers/storage`][rutebanken-storage] based implementation for storage. Must be combined with one of the supported extra profiles (see below). |

[rutebanken-storage]: https://github.com/entur/rutebanken-helpers/tree/master/storage

###### Supported `rutebanken-blobstore` extra profiles

If this profile is chosen, an additional implementation must be chosen to activate the underlying actual implementation.
Supported extra profiles are

| extra profile | description |
|:-----------------------|------------------------------------------|
| `local-disk-blobstore` | Similar to `local-blobstore`. |
| `in-memory-blobstore` | Entirely in-memory based implementation. |
| `s3-blobstore` | AWS S3 implementation. |

**Example: Activating `in-memory-blobstore` for local development**
```properties
spring.profiles.active=local,rutebanken-blobstore,in-memory-blobstore,local-changelog
```

See the [`RutebankenBlobStoreServiceConfiguration`](./src/main/java/org/rutebanken/tiamat/config/RutebankenBlobStoreConfiguration.java)
class for configuration keys and additional information.

##### Changelog

Expand All @@ -131,6 +173,28 @@ See [Docker Compose reference](https://docs.docker.com/compose/reference/) for m
| `activemq` | JMS based ActiveMQ implementation. |
| `google-pubsub` | GCP PubSub implementation for publishing tiamat entity changes. |

#### Supported Docker Compose Profiles

Tiamat's [`docker-compose.yml`](./docker-compose.yml) comes with built-in profiles for various use cases. The profiles
are mostly optional, default profile contains all mandatory configuration while the named profiles add features on
top of that. You can always activate zero or more profiles at the same time, e.g.

```shell
docker compose --profile first --profile second up
# or
COMPOSE_PROFILES=first,second docker compose up
```

### Default profile (no activation key)

Starts up PostGIS server with settings matching the ones in [`application-local.properties`](./src/main/resources/application-local.properties).

### `aws` profile

Starts up [LocalStack](https://www.localstack.cloud/) meant for developing AWS specific features.

See also [Disable AWS S3 Autoconfiguration](#disable-aws-s3-autoconfiguration), [NeTEx Export](#netex-export).

#### Run It!

**IntelliJ**: Right-click on `TiamatApplication.java` and choose Run (or Cmd+Shift+F10). Open Run -> Edit
Expand Down Expand Up @@ -467,4 +531,3 @@ https://github.com/entur/tiamat-scripts
## CircleCI
Tiamat is built using CircleCI. See the .circleci folder.


22 changes: 22 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,28 @@ services:
networks:
- tiamat-net

localstack:
container_name: "${COMPOSE_PROJECT_NAME}_localstack"
profiles: ["aws"]
image: localstack/localstack:4.0
ports:
- "37566:4566" # LocalStack Gateway
- "37510-37559:4510-4559" # external services port range
environment:
- DEBUG=${DEBUG-}
- DOCKER_HOST=unix:///var/run/docker.sock
- DISABLE_EVENTS=1
- SERVICES=s3
- AWS_ACCESS_KEY_ID=localstack
- AWS_SECRET_ACCESS_KEY=localstack
- AWS_DEFAULT_REGION=eu-north-1
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
- "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack"
- "./scripts/init-localstack.sh:/etc/localstack/init/ready.d/init-localstack.sh"
networks:
- tiamat-net

volumes:
postgres-data:

Expand Down
12 changes: 12 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
<xercesImpl.version>2.12.2</xercesImpl.version>
<maven.compiler.args>--add-opens java.base/java.lang=ALL-UNNAMED</maven.compiler.args>
<testcontainers.version>1.19.2</testcontainers.version>
<rutebanken-storage.version>4.7</rutebanken-storage.version>
</properties>

<dependencies>
Expand Down Expand Up @@ -129,6 +130,17 @@
<artifactId>metrics-graphite</artifactId>
</dependency>

<dependency>
<groupId>org.entur.ror.helpers</groupId>
<artifactId>storage</artifactId>
<version>${rutebanken-storage.version}</version>
</dependency>
<dependency>
<groupId>org.entur.ror.helpers</groupId>
<artifactId>storage-aws-s3</artifactId>
<version>${rutebanken-storage.version}</version>
</dependency>

<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
Expand Down
4 changes: 4 additions & 0 deletions scripts/init-localstack.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash

# -- > Create S3 bucket for blob storage
awslocal s3api create-bucket --bucket 'tiamat-test' --create-bucket-configuration LocationConstraint=eu-north-1
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package org.rutebanken.tiamat.config;

import org.rutebanken.helper.aws.repository.S3BlobStoreRepository;
import org.rutebanken.helper.storage.repository.BlobStoreRepository;
import org.rutebanken.helper.storage.repository.InMemoryBlobStoreRepository;
import org.rutebanken.helper.storage.repository.LocalDiskBlobStoreRepository;
import org.rutebanken.tiamat.service.BlobStoreService;
import org.rutebanken.tiamat.service.RutebankenBlobStoreService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Profile;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.S3ClientBuilder;

import java.net.URI;
import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;

/**
* <a href="https://github.com/entur/rutebanken-helpers">rutebanken-helpers</a> based repository configuration for blob
* storing. Support is enabled by activating <code>rutebanken-blobstore</code> profile and then one of the following:
* <ul>
* <li><code>local-disk-blobstore</code></li>
* <li><code>in-memory-blobstore</code></li>
* </ul>
* <p>
* Tiamat also has its own storage abstraction, {@link org.rutebanken.tiamat.service.BlobStoreService}. An adapter is
* provided ({@link org.rutebanken.tiamat.service.RutebankenBlobStoreService}) which will be used if the
* <code>rutebanken-blobstore</code> profile is active. The adapted blob stores complement Tiamat's own implementation
* in non-conflicting manner, meaning that of Tiamat's blobstore profiles (<code>gcs-blobstore</code>, <code>local-blobstore</code>)
* neither will clash with what this configuration class provides.
* <p>
* The names of the supported profiles and configuration keys match with the ones in <a href="https://github.com/entur/uttu">entur/uttu</a>.
* There is a legacy naming problem with the <code>gcs-blobstore</code> profile and its configuration injection, but as
* this <code>rutebanken-helpers adaptation</code> is not needed for GCS support within Tiamat but for the other
* implementations it provides, in practice these do not clash. If the GCS implementation is ever to be taken into use,
* {@link org.rutebanken.tiamat.service.GcsBlobStoreService} first needs to be adapted directly, and additional feature
* gap fixing will probably need to be done in <code>rutebanken-helpers</code> as well.
*/
@Lazy
@Configuration
@Profile("rutebanken-blobstore")
public class RutebankenBlobStoreConfiguration {

@Bean
BlobStoreService blobStoreService(BlobStoreRepository blobStoreRepository) {
return new RutebankenBlobStoreService(blobStoreRepository);
}

@Profile("local-disk-blobstore")
@Bean
BlobStoreRepository localDiskBlobStoreRepository(
@Value("${blobstore.local.folder:files/blob}") String baseFolder,
@Value("${blobstore.local.container.name}") String containerName
) {
LocalDiskBlobStoreRepository localDiskBlobStoreRepository = new LocalDiskBlobStoreRepository(baseFolder);
localDiskBlobStoreRepository.setContainerName(containerName);
return localDiskBlobStoreRepository;
}

@Profile("in-memory-blobstore")
@Bean
BlobStoreRepository inMemoryBlobStoreRepository(
@Value("${blobstore.local.container.name}") String containerName
) {
InMemoryBlobStoreRepository inMemoryBlobStoreRepository = new InMemoryBlobStoreRepository(new ConcurrentHashMap<>());
inMemoryBlobStoreRepository.setContainerName(containerName);
return inMemoryBlobStoreRepository;
}

@Bean
BlobStoreRepository blobStoreRepository(
@Value("${blobstore.s3.bucket}") String containerName,
S3Client s3Client
) {
S3BlobStoreRepository s3BlobStoreRepository = new S3BlobStoreRepository(s3Client);
s3BlobStoreRepository.setContainerName(containerName);
return s3BlobStoreRepository;
}

@Profile("local | test")
@Bean
public AwsCredentialsProvider localCredentials(
@Value("blobstore.s3.access-key-id") String accessKeyId,
@Value("blobstore.s3.secret-key") String secretKey
) {
return StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKeyId, secretKey)
);
}

@Profile("!local & !test")
@Bean
public AwsCredentialsProvider cloudCredentials() {
return DefaultCredentialsProvider.create();
}

@Bean
public S3Client s3Client(
@Value("${blobstore.s3.region}") String region,
@Value("${blobstore.s3.endpoint-override:#{null}}") String endpointOverride,
AwsCredentialsProvider credentialsProvider
) {
S3ClientBuilder builder = S3Client
.builder()
.region(Region.of(region))
.credentialsProvider(credentialsProvider)
.overrideConfiguration(
ClientOverrideConfiguration
.builder()
.apiCallAttemptTimeout(Duration.ofSeconds(15))
.apiCallTimeout(Duration.ofSeconds(15))
.retryPolicy(retryPolicy -> retryPolicy.numRetries(5))
.build()
);
if (endpointOverride != null) {
builder = builder.endpointOverride(URI.create(endpointOverride));
}
return builder.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
package org.rutebanken.tiamat.service;

import com.google.cloud.storage.Storage;

import java.io.InputStream;

public interface BlobStoreService {
void upload(String fileName, InputStream inputStream);

Storage getStorage();

InputStream download(String fileName);

String createBlobIdName(String blobPath, String fileName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ public class GcsBlobStoreService implements BlobStoreService {


public GcsBlobStoreService(@Value("${blobstore.gcs.credential.path:#{null}") String credentialPath,
@Value("${blobstore.gcs.bucket.name}") String bucketName,
@Value("${blobstore.gcs.blob.path}") String blobPath,
@Value("${blobstore.gcs.project.id}") String projectId) {
@Value("${blobstore.gcs.bucket.name}") String bucketName,
@Value("${blobstore.gcs.blob.path}") String blobPath,
@Value("${blobstore.gcs.project.id}") String projectId) {

this.bucketName = bucketName;
this.blobPath = blobPath;
Expand All @@ -61,7 +61,7 @@ public void upload(String fileName, InputStream inputStream) {
}
}

public Storage getStorage() {
private Storage getStorage() {
try {
logger.info("Get storage for project {}", projectId);
if (credentialPath == null || credentialPath.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.rutebanken.tiamat.service;

import com.google.cloud.storage.Storage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
Expand Down Expand Up @@ -59,14 +58,4 @@ public InputStream download(String fileName) {
throw new RuntimeException(e);
}
}

@Override
public String createBlobIdName(String blobPath, String fileName) {
return blobPath + '/' + fileName;
}

@Override
public Storage getStorage() {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.rutebanken.tiamat.service;

import org.rutebanken.helper.storage.model.BlobDescriptor;
import org.rutebanken.helper.storage.repository.BlobStoreRepository;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;

import java.io.InputStream;

/**
* Shim for adapting {@link BlobStoreService} to use <code>rutebanken-helpers</code>
* {@link org.rutebanken.helper.storage.repository.BlobStoreRepository} which enables reuse of additional storage
* implementations. See also {@link org.rutebanken.tiamat.config.RutebankenBlobStoreConfiguration}
*/
@Service
@Profile("rutebanken-blobstore")
public class RutebankenBlobStoreService implements BlobStoreService {

private final BlobStoreRepository blobStoreRepository;

public RutebankenBlobStoreService(BlobStoreRepository blobStoreRepository) {
this.blobStoreRepository = blobStoreRepository;
}

@Override
public void upload(String fileName, InputStream inputStream) {
blobStoreRepository.uploadBlob(new BlobDescriptor(fileName, inputStream));
}

@Override
public InputStream download(String fileName) {
return blobStoreRepository.getBlob(fileName);
}
}
Loading

0 comments on commit e62d146

Please # to comment.