Skip to content

Commit 6e23ec4

Browse files
authored
feat: Azure DataLake compatibility + metadata addition (#77)
* feat: Azure DataLake compatibility + metadata addition * style: Azure DataLake compatibility + metadata addition Incorporation of review comments (with the exception of one open query/counter-suggestion regarding the creation of a builder): Essentially - Removal of "final" for local variables - Removal of explicit paramaters for mock() where possible - "var" where possible The existing code, insofar as it is relevant for the Azure Blob extension, I have also adjusted. * style: Azure DataLake compatibility + metadata addition * style: Azure DataLake compatibility + metadata addition * chore: update dependencies
1 parent 200fb99 commit 6e23ec4

31 files changed

+988
-78
lines changed

DEPENDENCIES

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ maven/mavencentral/com.nimbusds/content-type/2.2, Apache-2.0, approved, clearlyd
101101
maven/mavencentral/com.nimbusds/lang-tag/1.7, Apache-2.0, approved, clearlydefined
102102
maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.25, Apache-2.0, approved, clearlydefined
103103
maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.30.2, Apache-2.0, approved, clearlydefined
104-
maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.37, , restricted, clearlydefined
104+
maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.37, Apache-2.0, approved, #11086
105105
maven/mavencentral/com.nimbusds/oauth2-oidc-sdk/10.7.1, Apache-2.0, approved, clearlydefined
106106
maven/mavencentral/com.puppycrawl.tools/checkstyle/10.0, LGPL-2.1-or-later, approved, #7936
107107
maven/mavencentral/com.squareup.okhttp3/logging-interceptor/3.12.12, Apache-2.0, approved, clearlydefined
@@ -148,9 +148,9 @@ maven/mavencentral/io.netty/netty-transport-native-epoll/4.1.94.Final, Apache-2.
148148
maven/mavencentral/io.netty/netty-transport-native-kqueue/4.1.94.Final, Apache-2.0 AND BSD-3-Clause AND MIT, approved, CQ20926
149149
maven/mavencentral/io.netty/netty-transport-native-unix-common/4.1.94.Final, Apache-2.0 AND BSD-3-Clause AND MIT, approved, CQ20926
150150
maven/mavencentral/io.netty/netty-transport/4.1.94.Final, Apache-2.0 AND BSD-3-Clause AND MIT, approved, CQ20926
151-
maven/mavencentral/io.opentelemetry.instrumentation/opentelemetry-instrumentation-annotations/1.31.0, , restricted, clearlydefined
152-
maven/mavencentral/io.opentelemetry/opentelemetry-api/1.31.0, , restricted, clearlydefined
153-
maven/mavencentral/io.opentelemetry/opentelemetry-context/1.31.0, , restricted, clearlydefined
151+
maven/mavencentral/io.opentelemetry.instrumentation/opentelemetry-instrumentation-annotations/1.31.0, Apache-2.0, approved, #11085
152+
maven/mavencentral/io.opentelemetry/opentelemetry-api/1.31.0, Apache-2.0, approved, #11087
153+
maven/mavencentral/io.opentelemetry/opentelemetry-context/1.31.0, Apache-2.0, approved, #11088
154154
maven/mavencentral/io.projectreactor.netty/reactor-netty-core/1.0.34, Apache-2.0, approved, #9687
155155
maven/mavencentral/io.projectreactor.netty/reactor-netty-http/1.0.34, Apache-2.0, approved, clearlydefined
156156
maven/mavencentral/io.projectreactor/reactor-core/3.4.31, Apache-2.0, approved, #7517

extensions/common/azure/azure-blob-core/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ plugins {
2020
dependencies {
2121
api(libs.edc.controlplane.spi)
2222
implementation(libs.azure.storageblob)
23+
implementation(libs.azure.identity)
2324
implementation(libs.edc.util)
2425

2526
testFixturesApi(libs.edc.core.dataPlane.util)

extensions/common/azure/azure-blob-core/src/main/java/org/eclipse/edc/azure/blob/AzureBlobStoreSchema.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,7 @@ private AzureBlobStoreSchema() {
2626
public static final String TYPE = "AzureStorage";
2727
public static final String CONTAINER_NAME = "container";
2828
public static final String ACCOUNT_NAME = "account";
29-
public static final String BLOB_NAME = "blobname";
29+
public static final String BLOB_NAME = "blobName";
30+
public static final String FOLDER_NAME = "folderName";
31+
public static final String CORRELATION_ID = "correlationId";
3032
}

extensions/common/azure/azure-blob-core/src/main/java/org/eclipse/edc/azure/blob/adapter/BlobAdapter.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.io.InputStream;
2020
import java.io.OutputStream;
21+
import java.util.Map;
2122

2223
/**
2324
* Adapter over {@link BlockBlobClient} in order to support mocking.
@@ -30,4 +31,6 @@ public interface BlobAdapter {
3031
String getBlobName();
3132

3233
long getBlobSize();
34+
35+
void setMetadata(Map<String, String> metadata);
3336
}

extensions/common/azure/azure-blob-core/src/main/java/org/eclipse/edc/azure/blob/adapter/DefaultBlobAdapter.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.io.InputStream;
2020
import java.io.OutputStream;
21+
import java.util.Map;
2122

2223
/**
2324
* Implementation of {@link BlobAdapter} using a {@link BlockBlobClient}.
@@ -48,4 +49,9 @@ public String getBlobName() {
4849
public long getBlobSize() {
4950
return client.getProperties().getBlobSize();
5051
}
52+
53+
@Override
54+
public void setMetadata(Map<String, String> metadata) {
55+
client.setMetadata(metadata);
56+
}
5157
}

extensions/common/azure/azure-blob-core/src/main/java/org/eclipse/edc/azure/blob/api/BlobStoreApi.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,42 @@ public interface BlobStoreApi {
4141

4242
byte[] getBlob(String account, String container, String blobName);
4343

44+
/**
45+
* Get a blob adapter containing convenience methods for working on blob objects on a storage account.
46+
* This method accepts storage account key credential, and it is used in a context, where unlimited access to
47+
* a storage account is required.
48+
*
49+
* @param accountName The name of the storage account
50+
* @param containerName The name of the container within the storage account
51+
* @param blobName The name of the blob within the container of the storage account
52+
* @param sharedKey The storage account key credential
53+
* @return The blob adapter corresponding to the blob specified by the input parameters
54+
*/
4455
BlobAdapter getBlobAdapter(String accountName, String containerName, String blobName, String sharedKey);
4556

57+
/**
58+
* Get a blob adapter containing convenience methods for working on blob objects on a storage account.
59+
* This method accepts a SAS (Shared Access Signature) token as a credential, and it's typically used for accessing
60+
* a storage account with limited sets of privileges. Pls. refer to the Azure SAS documentation for further details.
61+
*
62+
* @param accountName The name of the storage account
63+
* @param containerName The name of the container within the storage account
64+
* @param blobName The name of the blob within the container of the storage account
65+
* @param credential A valid SAS token
66+
* @return The blob adapter corresponding to the blob specified by the input parameters
67+
*/
4668
BlobAdapter getBlobAdapter(String accountName, String containerName, String blobName, AzureSasCredential credential);
69+
70+
/**
71+
* Get a blob adapter containing convenience methods for working on blob objects on a storage account.
72+
* This method doesn't require any credentials; it uses {@link com.azure.identity.DefaultAzureCredentialBuilder},
73+
* which contains an authentication flow consisting of several authentication mechanisms to be tried in a specific
74+
* order. Pls. refer to the official documentation for further details, i.e. the list of mechanisms tried.
75+
*
76+
* @param accountName The name of the storage account
77+
* @param containerName The name of the container within the storage account
78+
* @param blobName The name of the blob within the container of the storage account
79+
* @return The blob adapter corresponding to the blob specified by the input parameters
80+
*/
81+
BlobAdapter getBlobAdapter(String accountName, String containerName, String blobName);
4782
}

extensions/common/azure/azure-blob-core/src/main/java/org/eclipse/edc/azure/blob/api/BlobStoreApiImpl.java

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import com.azure.core.credential.AzureSasCredential;
1818
import com.azure.core.util.BinaryData;
19+
import com.azure.identity.DefaultAzureCredentialBuilder;
1920
import com.azure.storage.blob.BlobServiceClient;
2021
import com.azure.storage.blob.BlobServiceClientBuilder;
2122
import com.azure.storage.blob.models.BlobItem;
@@ -35,7 +36,6 @@
3536
import java.util.List;
3637
import java.util.Map;
3738
import java.util.Objects;
38-
import java.util.stream.Collectors;
3939

4040
public class BlobStoreApiImpl implements BlobStoreApi {
4141

@@ -65,30 +65,30 @@ public boolean exists(String accountName, String containerName) {
6565

6666
@Override
6767
public String createContainerSasToken(String accountName, String containerName, String permissionSpec, OffsetDateTime expiry) {
68-
BlobContainerSasPermission permissions = BlobContainerSasPermission.parse(permissionSpec);
69-
BlobServiceSasSignatureValues vals = new BlobServiceSasSignatureValues(expiry, permissions);
70-
return getBlobServiceClient(accountName).getBlobContainerClient(containerName).generateSas(vals);
68+
var permissions = BlobContainerSasPermission.parse(permissionSpec);
69+
var values = new BlobServiceSasSignatureValues(expiry, permissions);
70+
return getBlobServiceClient(accountName).getBlobContainerClient(containerName).generateSas(values);
7171
}
7272

7373
@Override
7474
public List<BlobItem> listContainer(String accountName, String containerName) {
75-
return getBlobServiceClient(accountName).getBlobContainerClient(containerName).listBlobs().stream().collect(Collectors.toList());
75+
return getBlobServiceClient(accountName).getBlobContainerClient(containerName).listBlobs().stream().toList();
7676
}
7777

7878
@Override
7979
public void putBlob(String accountName, String containerName, String blobName, byte[] data) {
80-
BlobServiceClient blobServiceClient = getBlobServiceClient(accountName);
80+
var blobServiceClient = getBlobServiceClient(accountName);
8181
blobServiceClient.getBlobContainerClient(containerName).getBlobClient(blobName).upload(BinaryData.fromBytes(data), true);
8282
}
8383

8484
@Override
8585
public String createAccountSas(String accountName, String containerName, String permissionSpec, OffsetDateTime expiry) {
86-
AccountSasPermission permissions = AccountSasPermission.parse(permissionSpec);
86+
var permissions = AccountSasPermission.parse(permissionSpec);
8787

88-
AccountSasService services = AccountSasService.parse("b");
89-
AccountSasResourceType resourceTypes = AccountSasResourceType.parse("co");
90-
AccountSasSignatureValues vals = new AccountSasSignatureValues(expiry, permissions, services, resourceTypes);
91-
return getBlobServiceClient(accountName).generateAccountSas(vals);
88+
var services = AccountSasService.parse("b");
89+
var resourceTypes = AccountSasResourceType.parse("co");
90+
var values = new AccountSasSignatureValues(expiry, permissions, services, resourceTypes);
91+
return getBlobServiceClient(accountName).generateAccountSas(values);
9292
}
9393

9494
@Override
@@ -110,8 +110,10 @@ private BlobServiceClient getBlobServiceClient(String accountName) {
110110
throw new IllegalArgumentException("No Object Storage credential found in vault!");
111111
}
112112

113-
BlobServiceClient blobServiceClient = new BlobServiceClientBuilder().credential(createCredential(accountKey, accountName))
114-
.endpoint(createEndpoint(accountName))
113+
var endpoint = createEndpoint(accountName);
114+
var blobServiceClient = new BlobServiceClientBuilder()
115+
.credential(createCredential(accountKey, accountName))
116+
.endpoint(endpoint)
115117
.buildClient();
116118

117119
cache.put(accountName, blobServiceClient);
@@ -124,13 +126,19 @@ private StorageSharedKeyCredential createCredential(String accountKey, String ac
124126

125127
@Override
126128
public BlobAdapter getBlobAdapter(String accountName, String containerName, String blobName, String sharedKey) {
127-
BlobServiceClientBuilder builder = new BlobServiceClientBuilder().credential(new StorageSharedKeyCredential(accountName, sharedKey));
129+
var builder = new BlobServiceClientBuilder().credential(new StorageSharedKeyCredential(accountName, sharedKey));
128130
return getBlobAdapter(accountName, containerName, blobName, builder);
129131
}
130132

131133
@Override
132134
public BlobAdapter getBlobAdapter(String accountName, String containerName, String blobName, AzureSasCredential credential) {
133-
BlobServiceClientBuilder builder = new BlobServiceClientBuilder().credential(credential);
135+
var builder = new BlobServiceClientBuilder().credential(credential);
136+
return getBlobAdapter(accountName, containerName, blobName, builder);
137+
}
138+
139+
@Override
140+
public BlobAdapter getBlobAdapter(String accountName, String containerName, String blobName) {
141+
var builder = new BlobServiceClientBuilder().credential(new DefaultAzureCredentialBuilder().build());
134142
return getBlobAdapter(accountName, containerName, blobName, builder);
135143
}
136144

extensions/common/azure/azure-blob-core/src/main/java/org/eclipse/edc/azure/blob/validator/AzureStorageValidator.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,17 @@ public class AzureStorageValidator {
3333
private static final int CONTAINER_MAX_LENGTH = 63;
3434
private static final int BLOB_MIN_LENGTH = 1;
3535
private static final int BLOB_MAX_LENGTH = 1024;
36+
private static final int METADATA_MIN_LENGTH = 1;
37+
private static final int METADATA_MAX_LENGTH = 4096;
3638
private static final Pattern ACCOUNT_REGEX = Pattern.compile("^[a-z0-9]+$");
3739
private static final Pattern CONTAINER_REGEX = Pattern.compile("^[a-z0-9]+(-[a-z0-9]+)*$");
40+
private static final Pattern METADATA_REGEX = Pattern.compile("^[ -~]*$"); // US-ASCII
3841

3942
private static final String ACCOUNT = "account";
4043
private static final String BLOB = "blob";
4144
private static final String CONTAINER = "container";
4245
private static final String KEY_NAME = "keyName";
46+
private static final String METADATA = "metadata";
4347
private static final String INVALID_RESOURCE_NAME = "Invalid %s name";
4448
private static final String INVALID_RESOURCE_NAME_LENGTH = "Invalid %s name length, the name must be between %s and %s characters long";
4549
private static final String RESOURCE_NAME_EMPTY = "Invalid %s name, the name may not be null, empty or blank";
@@ -91,6 +95,23 @@ public static void validateBlobName(String blobName) {
9195
}
9296
}
9397

98+
/**
99+
* Checks if a metadata value is valid.
100+
* The restriction is based on allowed characters for HTTP header values. As there is no length restriction per
101+
* header field, a reasonable restriction of 4096 character is assumed to leave some space, considering an
102+
* overall length restriction for HTTP headers of approximately 8KB.
103+
*
104+
* @param metadata A String representing the metadata value to validate.
105+
* @throws IllegalArgumentException if the string does not represent a valid metadata value.
106+
*/
107+
public static void validateMetadata(String metadata) {
108+
checkLength(metadata, METADATA, METADATA_MIN_LENGTH, METADATA_MAX_LENGTH);
109+
110+
if (!METADATA_REGEX.matcher(metadata).matches()) {
111+
throw new IllegalArgumentException(String.format(INVALID_RESOURCE_NAME, METADATA));
112+
}
113+
}
114+
94115
/**
95116
* Checks if key name is valid.
96117
*

extensions/common/azure/azure-blob-core/src/test/java/org/eclipse/edc/azure/blob/validator/AzureStorageValidatorTest.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,20 @@ void validateBlobName_fail(String input) {
9090
.isThrownBy(() -> AzureStorageValidator.validateBlobName(input));
9191
}
9292

93+
@ParameterizedTest
94+
@ValueSource(strings = { "abcdefghijklmnop", "-", "a/%!_- $K1~"})
95+
void validateMetadata_success(String input) {
96+
AzureStorageValidator.validateMetadata(input);
97+
}
98+
99+
@ParameterizedTest
100+
@ArgumentsSource(InvalidMetadataProvider.class)
101+
@NullAndEmptySource
102+
void validateMetadata_fail(String input) {
103+
assertThatExceptionOfType(IllegalArgumentException.class)
104+
.isThrownBy(() -> AzureStorageValidator.validateMetadata(input));
105+
}
106+
93107
private static class InvalidBlobNameProvider implements ArgumentsProvider {
94108
@Override
95109
public Stream<? extends Arguments> provideArguments(ExtensionContext context) throws Exception {
@@ -114,4 +128,13 @@ public Stream<? extends Arguments> provideArguments(ExtensionContext context) th
114128
Arguments.of("a/b".repeat(253)));
115129
}
116130
}
117-
}
131+
132+
private static class InvalidMetadataProvider implements ArgumentsProvider {
133+
@Override
134+
public Stream<? extends Arguments> provideArguments(ExtensionContext context) throws Exception {
135+
return Stream.of(
136+
Arguments.of("abcdefghijklmnop".repeat(256) + " a"),
137+
Arguments.of("a/%!_- $KÄ".repeat(64)));
138+
}
139+
}
140+
}

extensions/common/azure/azure-test/src/testFixtures/java/org/eclipse/edc/azure/testfixtures/TestFunctions.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,28 @@
1616

1717
import com.azure.storage.blob.BlobServiceClient;
1818
import com.azure.storage.blob.BlobServiceClientBuilder;
19+
import com.azure.storage.blob.specialized.BlockBlobClient;
1920
import com.azure.storage.common.StorageSharedKeyCredential;
2021
import org.jetbrains.annotations.NotNull;
2122

23+
import java.util.ArrayList;
24+
import java.util.List;
25+
2226
public final class TestFunctions {
2327
private TestFunctions() {
2428
}
2529

30+
@NotNull
31+
public static BlobServiceClient getBlobServiceClient(String accountName, String key) {
32+
var connectionString = new LocalConnectionStringBuilder()
33+
.http().account(accountName).key(key).endpoints(accountName).build();
34+
var client = new BlobServiceClientBuilder()
35+
.connectionString(connectionString)
36+
.buildClient();
37+
client.getAccountInfo();
38+
return client;
39+
}
40+
2641
@NotNull
2742
public static BlobServiceClient getBlobServiceClient(String accountName, String key, String endpoint) {
2843
var client = new BlobServiceClientBuilder()
@@ -35,7 +50,55 @@ public static BlobServiceClient getBlobServiceClient(String accountName, String
3550
}
3651

3752
@NotNull
53+
public static BlockBlobClient getBlobClient(String accountName, String containerName, String blobName, String token) {
54+
var connectionString = new LocalConnectionStringBuilder()
55+
.http().account(accountName).sharedAccessSignature(token).endpoints(accountName).build();
56+
var serviceClient = new BlobServiceClientBuilder()
57+
.connectionString(connectionString)
58+
.buildClient();
59+
return serviceClient.getBlobContainerClient(containerName).getBlobClient(blobName).getBlockBlobClient();
60+
}
61+
3862
public static String getBlobServiceTestEndpoint(String accountName) {
3963
return "http://127.0.0.1:10000/" + accountName;
4064
}
65+
66+
private static class LocalConnectionStringBuilder {
67+
68+
private final List<String> elements = new ArrayList<>();
69+
70+
public String build() {
71+
return String.join(";", elements) + ";";
72+
}
73+
74+
public LocalConnectionStringBuilder endpoints(String accountName) {
75+
elements.add("BlobEndpoint=http://127.0.0.1:10000/" + accountName);
76+
77+
// Even though not used, a malformed connection string exception is thrown if not present
78+
elements.add("QueueEndpoint=http://127.0.0.1:10001/" + accountName);
79+
elements.add("TableEndpoint=http://127.0.0.1:10002/" + accountName);
80+
elements.add("FileEndpoint=http://127.0.0.1:10003/" + accountName);
81+
return this;
82+
}
83+
84+
public LocalConnectionStringBuilder http() {
85+
elements.add("DefaultEndpointsProtocol=http");
86+
return this;
87+
}
88+
89+
public LocalConnectionStringBuilder account(String accountName) {
90+
elements.add("AccountName=" + accountName);
91+
return this;
92+
}
93+
94+
public LocalConnectionStringBuilder key(String accountKey) {
95+
elements.add("AccountKey=" + accountKey);
96+
return this;
97+
}
98+
99+
public LocalConnectionStringBuilder sharedAccessSignature(String sharedAccessSignature) {
100+
elements.add("SharedAccessSignature=" + sharedAccessSignature);
101+
return this;
102+
}
103+
}
41104
}

0 commit comments

Comments
 (0)