Skip to content

Commit 04dfc1f

Browse files
committed
added s3 provisioning of bucket
1 parent 37996f1 commit 04dfc1f

File tree

12 files changed

+272
-8
lines changed

12 files changed

+272
-8
lines changed

.dockerignore

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ Makefile
66
.*
77
!.mvn
88
run.sh
9+
jwt.txt

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ target/
33
!.mvn/wrapper/maven-wrapper.jar
44
!**/src/main/**/target/
55
!**/src/test/**/target/
6+
jwt.txt
67

78
### STS ###
89
.apt_generated

README.md

+17
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,23 @@ It is possible to run the dms locally while controlling a remote docker engine.
1414
DOCKER_HOST=unix:///some/local/path/to/docker.sock
1515
```
1616

17+
### Test Score uploads and downloads
18+
To test the score uploads and downloads, ego, song and score services must be running and healthy. Since the score service responds with presigned s3 urls that use the minio-api as the service name, the urls would not be resolvable outside of the defined `docker-swarm-network`. To fix this, a convienece docker image state was created in the Dockerfile (target is called `genomic-transfer-helper`) which pulls the appropriate song-client and score-client distributions and configures them with the right JWT. The following instructions will allow you to download and upload using this tool:
19+
1. Log in to the EGO-UI service. The default local url is http://localhost:9002
20+
2. Log out
21+
3. By default, new users obtain the role `USER`. In order to proceed, the `ADMIN` role is needed. To do this, run the following command
22+
```
23+
docker exec -it $(docker service ps ego-db --no-trunc --format '{{ .Name }}.{{.ID}}' | head -1) psql -U postgres ego -c "UPDATE egouser set type='ADMIN' where email='<the-email-you-logged-in-with-previously>' and providertype='<one of: GOOGLE,LINKEDIN,FACEBOOK,GITHUB,ORCID>';"
24+
```
25+
5. Log in again into EGO-UI (http://localhost:9002) and now you should be able to use the UI
26+
6. Select `Users` in the side bar, and then select your user record. On the right most pane, click the `Edit` button at the top and then click the `+ Add` button for the `Groups` section, and add your self to the `dcc-admin` group. Then click the `Save` button at the top. This will give you the neccessary permissions to use song and score.
27+
7. Log out
28+
8. Start the browsers inspector tool (this will be different for each browser) and filter for XHR requests.
29+
9. Log in to the EGO-UI. Once logged in, refer to response for the `ego-token?client_id=ego-ui` request. This is the JWT
30+
10. Copy and paste the JWT from the previous step into the file `./jwt.txt`.
31+
11. Run `make start-transfer-shell`. This will automatically run the `genomic-transfer-helper` and load the contents of `jwt.txt` as the JWT to allow authorized access to song and score.
32+
12. Once logged into the container, you can use the score and song clients. Run `./song-client/bin/sing ping` and `curl $(curl -sL http://score-api:8080/download/ping)` to do a health check.
33+
1734
## Tips
1835
### Checking for OOM messages when a container/service is killed
1936
Sometimes, if the reserved/limit memory is too low, a container will get killed by the kernel. To find out if this is the case, run

pom.xml

+9
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,13 @@
144144
<version>${jline.version}</version>
145145
</dependency>
146146

147+
<!-- AWS S3 -->
148+
<dependency>
149+
<groupId>software.amazon.awssdk</groupId>
150+
<artifactId>s3</artifactId>
151+
<version>${aws.java.sdk.version}</version>
152+
</dependency>
153+
147154

148155
<!-- Retry -->
149156
<dependency>
@@ -388,6 +395,8 @@
388395
<okhttp.version>4.9.0</okhttp.version>
389396
<spring-cloud-stub-runner.version>3.0.0</spring-cloud-stub-runner.version>
390397
<asm.version>8.0.1</asm.version>
398+
<aws.java.sdk.version>2.15.69</aws.java.sdk.version>
399+
391400

392401
<!-- Plugins-->
393402
<plugin.fmt.version>2.10</plugin.fmt.version>

src/main/java/bio/overture/dms/cli/questionnaire/ScoreQuestionnaire.java

+1
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ private ScoreS3Config processScoreS3Config(URL dmsGatewayUrl, ClusterRunModes cl
135135

136136
// https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints
137137
val awsS3Url = new URL("https://s3." + awsS3Region + ".amazonaws.com");
138+
s3Builder.s3Region(awsS3Region);
138139
s3Builder.url(awsS3Url);
139140
} else {
140141
val externalS3Url =

src/main/java/bio/overture/dms/compose/deployment/score/ScoreApiDeployer.java

+86-6
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,62 @@
11
package bio.overture.dms.compose.deployment.score;
22

3-
import static bio.overture.dms.compose.deployment.SimpleProvisionService.createSimpleProvisionService;
4-
import static bio.overture.dms.compose.model.ComposeServiceResources.SCORE_API;
5-
import static bio.overture.dms.compose.model.Constants.DMS_ADMIN_GROUP_NAME;
6-
73
import bio.overture.dms.compose.deployment.ServiceDeployer;
84
import bio.overture.dms.compose.deployment.ego.EgoHelper;
5+
import bio.overture.dms.compose.model.S3ObjectUploadRequest;
96
import bio.overture.dms.core.model.dmsconfig.DmsConfig;
107
import bio.overture.dms.core.model.dmsconfig.EgoConfig;
8+
import bio.overture.dms.core.model.dmsconfig.ScoreConfig;
119
import bio.overture.dms.core.model.dmsconfig.ScoreConfig.ScoreApiConfig;
10+
import bio.overture.dms.core.model.dmsconfig.ScoreConfig.ScoreS3Config;
11+
import bio.overture.dms.core.model.enums.ClusterRunModes;
12+
import bio.overture.dms.swarm.properties.DockerProperties;
1213
import lombok.NonNull;
14+
import lombok.SneakyThrows;
1315
import lombok.extern.slf4j.Slf4j;
1416
import lombok.val;
1517
import org.springframework.beans.factory.annotation.Autowired;
1618
import org.springframework.stereotype.Component;
19+
import software.amazon.awssdk.regions.Region;
20+
21+
import java.net.URI;
22+
23+
import static bio.overture.dms.compose.deployment.SimpleProvisionService.createSimpleProvisionService;
24+
import static bio.overture.dms.compose.deployment.score.s3.S3ServiceFactory.buildS3Service;
25+
import static bio.overture.dms.compose.model.ComposeServiceResources.MINIO_API;
26+
import static bio.overture.dms.compose.model.ComposeServiceResources.SCORE_API;
27+
import static bio.overture.dms.compose.model.Constants.DMS_ADMIN_GROUP_NAME;
28+
import static bio.overture.dms.core.model.enums.ClusterRunModes.LOCAL;
29+
import static bio.overture.dms.core.model.enums.ClusterRunModes.PRODUCTION;
30+
import static java.lang.String.format;
31+
import static software.amazon.awssdk.regions.Region.US_EAST_1;
1732

1833
@Slf4j
1934
@Component
2035
public class ScoreApiDeployer {
2136

37+
2238
/** Constants */
2339
private static final String SCORE_POLICY_NAME = "SCORE";
40+
private static final String HELIOGRAPH_CONTENT = "DMS Score Heliograph Object";
41+
private static final Region DEFAULT_S3_REGION = US_EAST_1;
42+
43+
// NOTE: the DEFAULT_OBJECT_PATH is baked into the score-api.yaml.vm
44+
private static final String DEFAULT_OBJECT_PATH = "data";
45+
// NOTE: the DEFAULT_OBJECT_SENTINEL is baked into the score-api.yaml.vm
46+
private static final String DEFAULT_OBJECT_SENTINEL = "heliograph";
47+
private static final int MINIO_API_CONTAINER_PORT = 9000;
2448

2549
/** Dependencies */
2650
private final ServiceDeployer serviceDeployer;
27-
2851
private final EgoHelper egoHelper;
52+
private final DockerProperties dockerProperties;
2953

3054
@Autowired
31-
public ScoreApiDeployer(@NonNull ServiceDeployer serviceDeployer, @NonNull EgoHelper egoHelper) {
55+
public ScoreApiDeployer(@NonNull ServiceDeployer serviceDeployer, @NonNull EgoHelper egoHelper,
56+
@NonNull DockerProperties dockerProperties) {
3257
this.serviceDeployer = serviceDeployer;
3358
this.egoHelper = egoHelper;
59+
this.dockerProperties = dockerProperties;
3460
}
3561

3662
public void deploy(@NonNull DmsConfig dmsConfig) {
@@ -41,6 +67,47 @@ public void deploy(@NonNull DmsConfig dmsConfig) {
4167

4268
private void provision(DmsConfig dmsConfig) {
4369
buildEgoScoreProvisioner(dmsConfig.getEgo(), dmsConfig.getScore().getApi()).run();
70+
provisionS3Buckets(dmsConfig.getClusterRunMode(), dmsConfig.getScore());
71+
}
72+
73+
private void provisionS3Buckets(ClusterRunModes clusterRunMode, ScoreConfig scoreConfig) {
74+
// TODO: May want to create interactive question for this in the future. For now, autocreate
75+
// buckets if minio is run
76+
val autoCreateBuckets = clusterRunMode == LOCAL;
77+
val endpointUri = resolveS3Endpoint(clusterRunMode, scoreConfig.getS3());
78+
val region = resolveS3Region(scoreConfig.getS3());
79+
val s3Service = buildS3Service(scoreConfig.getS3(), endpointUri, region);
80+
s3Service.provisionBucket(scoreConfig.getApi().getStateBucket(), autoCreateBuckets);
81+
s3Service.uploadData(
82+
S3ObjectUploadRequest.builder()
83+
.autoCreateBucket(autoCreateBuckets)
84+
.bucketName(scoreConfig.getApi().getObjectBucket())
85+
.objectKey(resolveObjectKey())
86+
.data(HELIOGRAPH_CONTENT.getBytes())
87+
.build());
88+
}
89+
90+
@SneakyThrows
91+
private URI resolveS3Endpoint(
92+
ClusterRunModes clusterRunMode, ScoreS3Config scoreS3Config) {
93+
if (clusterRunMode == PRODUCTION) {
94+
return scoreS3Config.getUrl().toURI();
95+
} else if (clusterRunMode == LOCAL) {
96+
return resolveMinioContainerUri(scoreS3Config);
97+
} else {
98+
throw new IllegalStateException(
99+
format(
100+
"The clusterRunMode '%s' is unknown and cannot be processed", clusterRunMode.name()));
101+
}
102+
}
103+
104+
@SneakyThrows
105+
private URI resolveMinioContainerUri(ScoreS3Config scoreS3Config) {
106+
if(dockerProperties.getRunAs()){
107+
return new URI("http://" + MINIO_API.toString() + ":" + MINIO_API_CONTAINER_PORT);
108+
} else {
109+
return scoreS3Config.getUrl().toURI();
110+
}
44111
}
45112

46113
private EgoScoreProvisioner buildEgoScoreProvisioner(
@@ -54,4 +121,17 @@ private EgoScoreProvisioner buildEgoScoreProvisioner(
54121
.scoreAppCredential(scoreApiConfig.getScoreAppCredential())
55122
.build();
56123
}
124+
125+
private static Region resolveS3Region(ScoreS3Config scoreS3Config){
126+
if (scoreS3Config.isS3RegionDefined()){
127+
return Region.of(scoreS3Config.getS3Region());
128+
} else {
129+
return DEFAULT_S3_REGION;
130+
}
131+
}
132+
133+
private static String resolveObjectKey() {
134+
return DEFAULT_OBJECT_PATH + "/" + DEFAULT_OBJECT_SENTINEL;
135+
}
136+
57137
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package bio.overture.dms.compose.deployment.score.s3;
2+
3+
import static bio.overture.dms.core.util.Exceptions.checkState;
4+
5+
import bio.overture.dms.compose.model.S3ObjectUploadRequest;
6+
import lombok.NonNull;
7+
import lombok.RequiredArgsConstructor;
8+
import lombok.SneakyThrows;
9+
import lombok.extern.slf4j.Slf4j;
10+
import lombok.val;
11+
import software.amazon.awssdk.core.sync.RequestBody;
12+
import software.amazon.awssdk.services.s3.S3Client;
13+
import software.amazon.awssdk.services.s3.model.Bucket;
14+
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
15+
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
16+
17+
@Slf4j
18+
@RequiredArgsConstructor
19+
public class S3Service {
20+
21+
@NonNull private final S3Client s3;
22+
23+
/** Provisions the request bucket followed by uploading data to the specified object key path */
24+
@SneakyThrows
25+
public PutObjectResponse uploadData(@NonNull S3ObjectUploadRequest request) {
26+
provisionBucket(request.getBucketName(), request.isAutoCreateBucket());
27+
log.info(
28+
"Uploading data to bucket '{}' with objectKey '{}'",
29+
request.getBucketName(),
30+
request.getObjectKey());
31+
val resp = putObject(request.getBucketName(), request.getObjectKey(), request.getData());
32+
log.info("- Done uploading data to bucket");
33+
return resp;
34+
}
35+
36+
/**
37+
* Creates a bucket if it does not exist and autoCreateBucket is true. If autoCreateBucket is
38+
* false, an error is thrown. If the bucket already exists, then bucket creation is skipped.
39+
*
40+
* @param bucketName to create
41+
* @param autoCreateBucket indicated whether the bucket should be created if it does not exist.
42+
*/
43+
public void provisionBucket(@NonNull String bucketName, boolean autoCreateBucket) {
44+
if (!autoCreateBucket) {
45+
checkBucketExists(bucketName);
46+
} else if (!isBucketExist(bucketName)) {
47+
log.info("Creating non-existent bucket '{}'", bucketName);
48+
createBucket(bucketName);
49+
log.info("- Done");
50+
} else {
51+
log.info("The bucket '{}' already exists, skipping bucket creation.", bucketName);
52+
}
53+
}
54+
55+
private PutObjectResponse putObject(String bucketName, String objectKey, byte[] data) {
56+
return s3.putObject(
57+
PutObjectRequest.builder().bucket(bucketName).key(objectKey).build(),
58+
RequestBody.fromBytes(data));
59+
}
60+
61+
private boolean isBucketExist(@NonNull String bucketName) {
62+
return s3.listBuckets().buckets().stream()
63+
.map(Bucket::name)
64+
.anyMatch(x -> x.equals(bucketName));
65+
}
66+
67+
private void checkBucketExists(String bucketName) {
68+
checkState(isBucketExist(bucketName), "The bucket '%s' does not exist", bucketName);
69+
}
70+
71+
private void createBucket(@NonNull String bucketName) {
72+
s3.createBucket(x -> x.bucket(bucketName));
73+
}
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package bio.overture.dms.compose.deployment.score.s3;
2+
3+
import static lombok.AccessLevel.PRIVATE;
4+
import static software.amazon.awssdk.core.client.config.SdkAdvancedClientOption.SIGNER;
5+
import static software.amazon.awssdk.core.retry.RetryMode.STANDARD;
6+
import static software.amazon.awssdk.regions.Region.US_EAST_1;
7+
8+
import bio.overture.dms.core.model.dmsconfig.ScoreConfig.ScoreS3Config;
9+
import java.net.URI;
10+
import java.time.Duration;
11+
import java.util.Map;
12+
import lombok.NoArgsConstructor;
13+
import lombok.NonNull;
14+
import lombok.SneakyThrows;
15+
import lombok.val;
16+
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
17+
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
18+
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
19+
import software.amazon.awssdk.auth.signer.AwsS3V4Signer;
20+
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
21+
import software.amazon.awssdk.regions.Region;
22+
import software.amazon.awssdk.services.s3.S3Client;
23+
24+
@NoArgsConstructor(access = PRIVATE)
25+
public class S3ServiceFactory {
26+
27+
private static final Duration API_CALL_TIMEOUT_DURATION = Duration.ofMillis(15000);
28+
public static S3Service buildS3Service(
29+
@NonNull ScoreS3Config scoreS3Config, @NonNull URI s3EndpointUri, Region region) {
30+
val s3 = buildS3Client(scoreS3Config, s3EndpointUri, region);
31+
return new S3Service(s3);
32+
}
33+
34+
@SneakyThrows
35+
private static S3Client buildS3Client(ScoreS3Config scoreS3Config, URI s3EndpointUri, Region region) {
36+
val b = S3Client.builder();
37+
b.region(region);
38+
b.endpointOverride(s3EndpointUri);
39+
b.credentialsProvider(buildAwsCredentialsProvider(scoreS3Config));
40+
b.overrideConfiguration(buildS3Configuration(API_CALL_TIMEOUT_DURATION));
41+
return b.build();
42+
}
43+
44+
private static ClientOverrideConfiguration buildS3Configuration(Duration callTimout) {
45+
return ClientOverrideConfiguration.builder()
46+
.apiCallTimeout(callTimout)
47+
.retryPolicy(STANDARD)
48+
.advancedOptions(Map.of(SIGNER, AwsS3V4Signer.create()))
49+
.build();
50+
}
51+
52+
private static AwsCredentialsProvider buildAwsCredentialsProvider(ScoreS3Config scoreS3Config) {
53+
return StaticCredentialsProvider.create(
54+
AwsBasicCredentials.create(scoreS3Config.getAccessKey(), scoreS3Config.getSecretKey()));
55+
}
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package bio.overture.dms.compose.model;
2+
3+
import lombok.Builder;
4+
import lombok.NonNull;
5+
import lombok.Value;
6+
7+
@Value
8+
@Builder
9+
public class S3ObjectUploadRequest {
10+
11+
private final boolean autoCreateBucket;
12+
@NonNull private final String bucketName;
13+
@NonNull private final String objectKey;
14+
@NonNull private final byte[] data;
15+
}

src/main/java/bio/overture/dms/core/model/dmsconfig/ScoreConfig.java

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package bio.overture.dms.core.model.dmsconfig;
22

3+
import static bio.overture.dms.core.util.Strings.isDefined;
34
import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY;
45

56
import com.fasterxml.jackson.annotation.JsonInclude;
@@ -36,6 +37,11 @@ public static class ScoreS3Config {
3637
// These are optional
3738
private boolean useMinio;
3839
private Integer hostPort;
40+
private String s3Region;
41+
42+
public boolean isS3RegionDefined(){
43+
return isDefined(s3Region);
44+
}
3945
}
4046

4147
@Data

src/main/resources/application.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ compose:
2424
network: dms-swarm-network
2525

2626
docker:
27-
#NOTE: Indicates if this application is RUNNING-AS a docker container on the host machine, or running on baremetal on the host machine. This is mostly to help resolve the what URLs the DMS uses to interact with the services. If its RUNNING-AS a docker container, then it will be in the dms dswarm network, and so it can use the service names as the domain in the urls, since docker will resolve the IP. If its not, then the url for the service will be used.
27+
#NOTE: Indicates if this application is RUNNING-AS a docker container on the host machine. If false, then it is running on bare-metal on the host machine. This is mostly to help resolve the what URLs the DMS uses to interact with the services. If its RUNNING-AS a docker container, then it will be in the dms swarm network, and so it can use the service names as the domain in the urls, since docker will resolve the IP. If its not, then the url for the service will be used.
2828
run-as: false
29-
# If blank, will use the system default docker daemon
29+
# If blank, will use the system default docker daemon. Useful for using a remote docker daemon.
3030
host: ""

src/main/resources/templates/servicespec/score-api.yaml.vm

+4
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ TaskTemplate:
1616
- UPLOAD_PARTSIZE=1073741824
1717
- UPLOAD_CONNECTION_TIMEOUT=1200000
1818
- METADATA_URL=http://${composeServiceResources.SONG_API.toString()}:8080
19+
#if( $dmsConfig.clusterRunMode == 'PRODUCTION')
1920
- S3_ENDPOINT=$dmsConfig.score.s3.url
21+
#elseif( $dmsConfig.clusterRunMode == 'LOCAL')
22+
- S3_ENDPOINT=http://${composeServiceResources.MINIO_API.toString()}:9000
23+
#end
2024
- S3_ACCESSKEY=$dmsConfig.score.s3.accessKey
2125
- S3_SECRETKEY=$dmsConfig.score.s3.secretKey
2226
- S3_SIGV4ENABLED=true

0 commit comments

Comments
 (0)