Skip to content
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

feat: Support available credentials providers for GCP blob storage #239 #245

Merged
merged 8 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ Static settings are used on startup and cannot be changed while application is r
| client.* | - | Vertx HTTP client settings for outbound requests.
| storage.provider | - | Specifies blob storage provider. Supported providers: s3, aws-s3, azureblob, google-cloud-storage, filesystem
| storage.endpoint | - | Optional. Specifies endpoint url for s3 compatible storages
| storage.identity | - | Blob storage access key. Can be optional for filesystem and aws-s3 providers
| storage.credential | - | Blob storage secret key. Can be optional for filesystem and aws-s3 providers
| storage.identity | - | Blob storage access key. Can be optional for filesystem, aws-s3, google-cloud-storage providers
| storage.credential | - | Blob storage secret key. Can be optional for filesystem, aws-s3, google-cloud-storage providers
| storage.bucket | - | Blob storage bucket
| storage.overrides.* | - | Key-value pairs to override storage settings
| storage.createBucket | false | Indicates whether bucket should be created on start-up
Expand All @@ -70,6 +70,32 @@ Static settings are used on startup and cannot be changed while application is r
| redis.clusterServersConfig.nodeAddresses | - | Json array with Redis cluster server addresses, e.g. ["redis://host1:port1","redis://host2:port2"]
| invitations.ttlInSeconds | 259200 | Invitation time to live in seconds

### Google Cloud Storage

There are two types of credentials providers supported:
- User credentials. You can create a service account and authenticate using its private key obtained from Developer console
- Temporary credentials. Application default credentials (ADC)

#### User credentials

You should set `storage.credential` to a path to private key JSON file. `storage.identity` may be empty.

#### Temporary credentials

You should follow [instructions](https://cloud.google.com/kubernetes-engine/docs/concepts/workload-identity) to setup your pod in GKE.
`storage.credential` and `storage.identity` must be unset.
JClouds property `jclouds.oauth.credential-type` should be set `bearerTokenCredentials`, e.g.

```
{
"storage": {
"overrides": {
"jclouds.oauth.credential-type": "bearerTokenCredentials"
}
}
}
```

### Redis
The Redis can be used as a cache with volatile-* eviction policies:
```
Expand Down
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ dependencies {
implementation 'org.redisson:redisson:3.25.1'
implementation group: 'com.amazonaws', name: 'aws-java-sdk-core', version: '1.12.663'
implementation group: 'com.amazonaws', name: 'aws-java-sdk-sts', version: '1.12.663'
implementation group: 'com.google.auth', name: 'google-auth-library-oauth2-http', version: '1.23.0'


runtimeOnly 'com.epam.deltix:gflog-slf4j:3.0.5'

Expand Down
12 changes: 3 additions & 9 deletions src/main/java/com/epam/aidial/core/storage/BlobStorage.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import com.epam.aidial.core.data.MetadataBase;
import com.epam.aidial.core.data.ResourceFolderMetadata;
import com.epam.aidial.core.data.ResourceType;
import com.epam.aidial.core.storage.credential.CredentialProvider;
import com.epam.aidial.core.storage.credential.CredentialProviderFactory;
import io.vertx.core.buffer.Buffer;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -63,7 +65,7 @@ public BlobStorage(Storage config) {
if (overrides != null) {
builder.overrides(overrides);
}
CredentialProvider credentialProvider = getCredentialProvider(StorageProvider.from(provider), config.getIdentity(), config.getCredential());
CredentialProvider credentialProvider = CredentialProviderFactory.create(provider, config.getIdentity(), config.getCredential());
builder.credentialsSupplier(credentialProvider::getCredentials);
this.storeContext = builder.buildView(BlobStoreContext.class);
this.blobStore = storeContext.getBlobStore();
Expand Down Expand Up @@ -302,14 +304,6 @@ private static ContentMetadata buildContentMetadata(String contentType) {
return BaseMutableContentMetadata.fromContentMetadata(contentMetadata);
}

private static CredentialProvider getCredentialProvider(StorageProvider provider, String identity, String credential) {
return switch (provider) {
case S3, AZURE_BLOB, GOOGLE_CLOUD_STORAGE -> new DefaultCredentialProvider(identity, credential);
case FILESYSTEM -> new DefaultCredentialProvider("identity", "credential");
case AWS_S3 -> new AwsCredentialProvider(identity, credential);
};
}

private void createBucketIfNeeded(Storage config) {
if (config.isCreateBucket() && !storeContext.getBlobStore().containerExists(bucketName)) {
storeContext.getBlobStore().createContainerInLocation(null, bucketName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,6 @@ public synchronized void abortUpload(Throwable ex) {
errorHandler.handle(ex);
}

log.warn("Multipart upload aborted");
log.warn("Multipart upload aborted", ex);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.epam.aidial.core.storage;
package com.epam.aidial.core.storage.credential;

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSSessionCredentials;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.epam.aidial.core.storage;
package com.epam.aidial.core.storage.credential;

import org.jclouds.domain.Credentials;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.epam.aidial.core.storage.credential;

import com.epam.aidial.core.storage.StorageProvider;
import lombok.experimental.UtilityClass;

@UtilityClass
public class CredentialProviderFactory {
public static CredentialProvider create(String providerName, String identity, String credential) {
StorageProvider provider = StorageProvider.from(providerName);
return switch (provider) {
case S3, AZURE_BLOB -> new DefaultCredentialProvider(identity, credential);
case GOOGLE_CLOUD_STORAGE -> new GcpCredentialProvider(credential);
case FILESYSTEM -> new DefaultCredentialProvider("identity", "credential");
case AWS_S3 -> new AwsCredentialProvider(identity, credential);
};
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.epam.aidial.core.storage;
package com.epam.aidial.core.storage.credential;

import org.jclouds.domain.Credentials;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.epam.aidial.core.storage.credential;

import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.common.io.Files;
import lombok.SneakyThrows;
import org.jclouds.domain.Credentials;
import org.jclouds.googlecloud.GoogleCredentialsFromJson;

import java.io.File;
import java.time.Instant;
import java.util.Date;

import static java.nio.charset.StandardCharsets.UTF_8;

public class GcpCredentialProvider implements CredentialProvider {

private static final long EXPIRATION_WINDOW_IN_MS = 10_000;

private Credentials credentials;

private AccessToken accessToken;

private GoogleCredentials googleCredentials;

@SneakyThrows
public GcpCredentialProvider(String pathToPrivateKey) {
if (pathToPrivateKey != null) {
this.credentials = getCredentialsFromJsonKeyFile(pathToPrivateKey);
} else {
this.googleCredentials = GoogleCredentials.getApplicationDefault();
}
}

@Override
public Credentials getCredentials() {
if (credentials != null) {
return credentials;
}
return getTemporaryCredentials();
}

@SneakyThrows
private synchronized Credentials getTemporaryCredentials() {
Date expireAt = Date.from(Instant.ofEpochMilli(System.currentTimeMillis() - EXPIRATION_WINDOW_IN_MS));
if (accessToken == null || expireAt.after(accessToken.getExpirationTime())) {
accessToken = googleCredentials.refreshAccessToken();
}
return new Credentials("", accessToken.getTokenValue());
}

@SneakyThrows
private static Credentials getCredentialsFromJsonKeyFile(String filename) {
String fileContents = Files.asCharSource(new File(filename), UTF_8).read();
GoogleCredentialsFromJson credentialSupplier = new GoogleCredentialsFromJson(fileContents);
return credentialSupplier.get();
}
}
Loading