You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by mi...@apache.org on 2022/02/08 16:11:09 UTC

[jackrabbit-oak] branch trunk updated: OAK-9680 Add support for Azure SAS URIs to oak-segment-azure

This is an automated email from the ASF dual-hosted git repository.

miroslav pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 82a33f5  OAK-9680 Add support for Azure SAS URIs to oak-segment-azure
     new fd32acb  Merge pull request #479 from jelmini/feature/sas_uri_support
82a33f5 is described below

commit 82a33f5187ca4149849dc0e92475dccb89082684
Author: Carlo Jelmini <je...@adobe.com>
AuthorDate: Mon Jan 31 19:56:46 2022 +0100

    OAK-9680 Add support for Azure SAS URIs to oak-segment-azure
    
    Allow using Shared Access Signature URI to connect to Azure Storage.
---
 oak-segment-azure/pom.xml                          |   6 +
 .../segment/azure/AzureSegmentStoreService.java    |  90 +++++--
 .../oak/segment/azure/Configuration.java           |  13 +-
 .../azure/AzureSegmentStoreServiceTest.java        | 262 +++++++++++++++++++++
 .../oak/segment/azure/AzuriteDockerRule.java       |  55 +++--
 5 files changed, 380 insertions(+), 46 deletions(-)

diff --git a/oak-segment-azure/pom.xml b/oak-segment-azure/pom.xml
index 95b95cf..04cf956 100644
--- a/oak-segment-azure/pom.xml
+++ b/oak-segment-azure/pom.xml
@@ -213,6 +213,12 @@
             <artifactId>logback-classic</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.testing.osgi-mock</artifactId>
+            <scope>test</scope>
+        </dependency>
+
     </dependencies>
 
 </project>
diff --git a/oak-segment-azure/src/main/java/org/apache/jackrabbit/oak/segment/azure/AzureSegmentStoreService.java b/oak-segment-azure/src/main/java/org/apache/jackrabbit/oak/segment/azure/AzureSegmentStoreService.java
index 11a9b75..489c4d1 100644
--- a/oak-segment-azure/src/main/java/org/apache/jackrabbit/oak/segment/azure/AzureSegmentStoreService.java
+++ b/oak-segment-azure/src/main/java/org/apache/jackrabbit/oak/segment/azure/AzureSegmentStoreService.java
@@ -21,7 +21,9 @@ package org.apache.jackrabbit.oak.segment.azure;
 import com.microsoft.azure.storage.CloudStorageAccount;
 import com.microsoft.azure.storage.StorageException;
 import com.microsoft.azure.storage.blob.CloudBlobContainer;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.jackrabbit.oak.segment.spi.persistence.SegmentNodeStorePersistence;
+import org.jetbrains.annotations.NotNull;
 import org.osgi.framework.ServiceRegistration;
 import org.osgi.service.component.ComponentContext;
 import org.osgi.service.component.annotations.Activate;
@@ -37,8 +39,8 @@ import java.security.InvalidKeyException;
 import java.util.Properties;
 
 @Component(
-        configurationPolicy = ConfigurationPolicy.REQUIRE,
-        configurationPid = {Configuration.PID})
+    configurationPolicy = ConfigurationPolicy.REQUIRE,
+    configurationPid = {Configuration.PID})
 public class AzureSegmentStoreService {
 
     private static final Logger log = LoggerFactory.getLogger(AzureSegmentStoreService.class);
@@ -49,12 +51,11 @@ public class AzureSegmentStoreService {
 
     private ServiceRegistration registration;
 
-    private SegmentNodeStorePersistence persistence;
-
     @Activate
     public void activate(ComponentContext context, Configuration config) throws IOException {
-        persistence = createAzurePersistence(config);
-        registration = context.getBundleContext().registerService(SegmentNodeStorePersistence.class.getName(), persistence, new Properties());
+        AzurePersistence persistence = createAzurePersistenceFrom(config);
+        registration = context.getBundleContext()
+            .registerService(SegmentNodeStorePersistence.class.getName(), persistence, new Properties());
     }
 
     @Deactivate
@@ -63,35 +64,72 @@ public class AzureSegmentStoreService {
             registration.unregister();
             registration = null;
         }
-        persistence = null;
     }
 
-    private static SegmentNodeStorePersistence createAzurePersistence(Configuration configuration) throws IOException {
+    private static AzurePersistence createAzurePersistenceFrom(Configuration configuration) throws IOException {
+        if (!StringUtils.isBlank(configuration.connectionURL())) {
+            return createPersistenceFromConnectionURL(configuration);
+        }
+        if (!StringUtils.isBlank(configuration.sharedAccessSignature())) {
+            return createPersistenceFromSasUri(configuration);
+        }
+        return createPersistenceFromAccessKey(configuration);
+    }
+
+    private static AzurePersistence createPersistenceFromAccessKey(Configuration configuration) throws IOException {
+        StringBuilder connectionString = new StringBuilder();
+        connectionString.append("DefaultEndpointsProtocol=https;");
+        connectionString.append("AccountName=").append(configuration.accountName()).append(';');
+        connectionString.append("AccountKey=").append(configuration.accessKey()).append(';');
+        if (!StringUtils.isBlank(configuration.blobEndpoint())) {
+            connectionString.append("BlobEndpoint=").append(configuration.blobEndpoint()).append(';');
+        }
+        return createAzurePersistence(connectionString.toString(), configuration, true);
+    }
+
+    private static AzurePersistence createPersistenceFromSasUri(Configuration configuration) throws IOException {
+        StringBuilder connectionString = new StringBuilder();
+        connectionString.append("DefaultEndpointsProtocol=https;");
+        connectionString.append("AccountName=").append(configuration.accountName()).append(';');
+        connectionString.append("SharedAccessSignature=").append(configuration.sharedAccessSignature()).append(';');
+        if (!StringUtils.isBlank(configuration.blobEndpoint())) {
+            connectionString.append("BlobEndpoint=").append(configuration.blobEndpoint()).append(';');
+        }
+        return createAzurePersistence(connectionString.toString(), configuration, false); 
+    }
+
+    @NotNull
+    private static AzurePersistence createPersistenceFromConnectionURL(Configuration configuration) throws IOException {
+        return createAzurePersistence(configuration.connectionURL(), configuration, true);
+    }
+
+    @NotNull
+    private static AzurePersistence createAzurePersistence(
+        String connectionString,
+        Configuration configuration,
+        boolean createContainer
+    ) throws IOException {
         try {
-            StringBuilder connectionString = new StringBuilder();
-            if (configuration.connectionURL() == null || configuration.connectionURL().trim().isEmpty()) {
-                connectionString.append("DefaultEndpointsProtocol=https;");
-                connectionString.append("AccountName=").append(configuration.accountName()).append(';');
-                connectionString.append("AccountKey=").append(configuration.accessKey()).append(';');
-            } else {
-                connectionString.append(configuration.connectionURL());
-            }
-            CloudStorageAccount cloud = CloudStorageAccount.parse(connectionString.toString());
-            log.info("Connection string: '{}'", cloud.toString());
+            CloudStorageAccount cloud = CloudStorageAccount.parse(connectionString);
+            log.info("Connection string: '{}'", cloud);
             CloudBlobContainer container = cloud.createCloudBlobClient().getContainerReference(configuration.containerName());
-            container.createIfNotExists();
-
-            String path = configuration.rootPath();
-            if (path != null && path.length() > 0 && path.charAt(0) == '/') {
-                path = path.substring(1);
+            if (createContainer) {
+                container.createIfNotExists();
             }
-
-            AzurePersistence persistence = new AzurePersistence(container.getDirectoryReference(path));
-            return persistence;
+            String path = normalizePath(configuration.rootPath());
+            return new AzurePersistence(container.getDirectoryReference(path));
         } catch (StorageException | URISyntaxException | InvalidKeyException e) {
             throw new IOException(e);
         }
     }
 
+    @NotNull
+    private static String normalizePath(@NotNull String rootPath) {
+        if (rootPath.length() > 0 && rootPath.charAt(0) == '/') {
+            return rootPath.substring(1);
+        }
+        return rootPath;
+    }
+
 }
 
diff --git a/oak-segment-azure/src/main/java/org/apache/jackrabbit/oak/segment/azure/Configuration.java b/oak-segment-azure/src/main/java/org/apache/jackrabbit/oak/segment/azure/Configuration.java
index 11da64d..e5cef06 100644
--- a/oak-segment-azure/src/main/java/org/apache/jackrabbit/oak/segment/azure/Configuration.java
+++ b/oak-segment-azure/src/main/java/org/apache/jackrabbit/oak/segment/azure/Configuration.java
@@ -54,6 +54,17 @@ import static org.apache.jackrabbit.oak.segment.azure.Configuration.PID;
     @AttributeDefinition(
             name = "Azure connection string (optional)",
             description = "Connection string to be used to connect to the Azure Storage. " +
-                    "Setting it will override the accountName and accessKey properties.")
+                    "Setting it will take precedence over accountName/accessKey and sharedAccessSignature properties.")
     String connectionURL() default "";
+
+    @AttributeDefinition(
+        name = "Azure Shared Access Signature (optional)",
+        description = "Shared Access Signature string to be used to connect to the Azure Storage. " +
+            "Setting it will take precedence over accountName/accessKey properties.")
+    String sharedAccessSignature() default "";
+
+    @AttributeDefinition(
+        name = "Azure Blob Endpoint URL (optional)",
+        description = "Blob Endpoint URL used to connect to the Azure Storage")
+    String blobEndpoint() default "";
 }
\ No newline at end of file
diff --git a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureSegmentStoreServiceTest.java b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureSegmentStoreServiceTest.java
new file mode 100644
index 0000000..f10ec24
--- /dev/null
+++ b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzureSegmentStoreServiceTest.java
@@ -0,0 +1,262 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.oak.segment.azure;
+
+import com.google.common.collect.ImmutableSet;
+import com.microsoft.azure.storage.StorageException;
+import com.microsoft.azure.storage.blob.*;
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+import java.net.URISyntaxException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.*;
+import java.util.stream.StreamSupport;
+import org.apache.jackrabbit.oak.segment.spi.persistence.SegmentNodeStorePersistence;
+import org.apache.sling.testing.mock.osgi.junit.OsgiContext;
+import org.jetbrains.annotations.NotNull;
+import org.junit.*;
+
+import static com.microsoft.azure.storage.blob.SharedAccessBlobPermissions.*;
+import static java.util.stream.Collectors.toSet;
+import static org.junit.Assert.*;
+
+public class AzureSegmentStoreServiceTest {
+    
+    @ClassRule
+    public static AzuriteDockerRule azurite = new AzuriteDockerRule();
+
+    @Rule
+    public final OsgiContext context = new OsgiContext();
+
+    private static final EnumSet<SharedAccessBlobPermissions> READ_ONLY = EnumSet.of(READ, LIST);
+    private static final EnumSet<SharedAccessBlobPermissions> READ_WRITE = EnumSet.of(READ, LIST, CREATE, WRITE, ADD);
+    private static final ImmutableSet<String> BLOBS = ImmutableSet.of("blob1", "blob2");
+    
+    private CloudBlobContainer container;
+    
+    @Before
+    public void setup() throws Exception {
+        container = azurite.getContainer(AzureSegmentStoreService.DEFAULT_CONTAINER_NAME);
+        for (String blob : BLOBS) {
+            container.getBlockBlobReference(blob + ".txt").uploadText(blob);
+        }
+    }
+
+    @Test
+    public void connectWithSharedAccessSignatureURL_readOnly() throws Exception {
+        String sasToken = container.generateSharedAccessSignature(policy(READ_ONLY), null);
+
+        AzureSegmentStoreService azureSegmentStoreService = new AzureSegmentStoreService();
+        azureSegmentStoreService.activate(context.componentContext(), getConfigurationWithSharedAccessSignature(sasToken));
+
+        SegmentNodeStorePersistence persistence = context.getService(SegmentNodeStorePersistence.class);
+        assertNotNull(persistence);
+        assertWriteAccessNotGranted(persistence);
+        assertReadAccessGranted(persistence, BLOBS);
+    }
+
+    @Test
+    public void connectWithSharedAccessSignatureURL_readWrite() throws Exception {
+        String sasToken = container.generateSharedAccessSignature(policy(READ_WRITE), null);
+
+        AzureSegmentStoreService azureSegmentStoreService = new AzureSegmentStoreService();
+        azureSegmentStoreService.activate(context.componentContext(), getConfigurationWithSharedAccessSignature(sasToken));
+
+        SegmentNodeStorePersistence persistence = context.getService(SegmentNodeStorePersistence.class);
+        assertNotNull(persistence);
+        assertWriteAccessGranted(persistence);
+        assertReadAccessGranted(persistence, concat(BLOBS, "test"));
+    }
+
+    @Test
+    public void connectWithSharedAccessSignatureURL_expired() throws Exception {
+        SharedAccessBlobPolicy expiredPolicy = policy(READ_WRITE, yesterday());
+        String sasToken = container.generateSharedAccessSignature(expiredPolicy, null);
+
+        AzureSegmentStoreService azureSegmentStoreService = new AzureSegmentStoreService();
+        azureSegmentStoreService.activate(context.componentContext(), getConfigurationWithSharedAccessSignature(sasToken));
+
+        SegmentNodeStorePersistence persistence = context.getService(SegmentNodeStorePersistence.class);
+        assertNotNull(persistence);
+        assertWriteAccessNotGranted(persistence);
+        assertReadAccessNotGranted(persistence);
+    }
+
+    @Test
+    public void connectWithAccessKey() throws Exception {
+        AzureSegmentStoreService azureSegmentStoreService = new AzureSegmentStoreService();
+        azureSegmentStoreService.activate(context.componentContext(), getConfigurationWithAccessKey(AzuriteDockerRule.ACCOUNT_KEY));
+
+        SegmentNodeStorePersistence persistence = context.getService(SegmentNodeStorePersistence.class);
+        assertNotNull(persistence);
+        assertWriteAccessGranted(persistence);
+        assertReadAccessGranted(persistence, concat(BLOBS, "test"));
+    }
+
+    @Test
+    public void connectWithConnectionURL() throws Exception {
+        AzureSegmentStoreService azureSegmentStoreService = new AzureSegmentStoreService();
+        azureSegmentStoreService.activate(context.componentContext(), getConfigurationWithConfigurationURL(AzuriteDockerRule.ACCOUNT_KEY));
+
+        SegmentNodeStorePersistence persistence = context.getService(SegmentNodeStorePersistence.class);
+        assertNotNull(persistence);
+        assertWriteAccessGranted(persistence);
+        assertReadAccessGranted(persistence, concat(BLOBS, "test"));
+    }
+
+    @Test
+    public void deactivate() throws Exception {
+        AzureSegmentStoreService azureSegmentStoreService = new AzureSegmentStoreService();
+        azureSegmentStoreService.activate(context.componentContext(), getConfigurationWithAccessKey(AzuriteDockerRule.ACCOUNT_KEY));
+        assertNotNull(context.getService(SegmentNodeStorePersistence.class));
+
+        azureSegmentStoreService.deactivate();
+        assertNull(context.getService(SegmentNodeStorePersistence.class));
+    }
+
+    @NotNull
+    private static SharedAccessBlobPolicy policy(EnumSet<SharedAccessBlobPermissions> permissions, Instant expirationTime) {
+        SharedAccessBlobPolicy sharedAccessBlobPolicy = new SharedAccessBlobPolicy();
+        sharedAccessBlobPolicy.setPermissions(permissions);
+        sharedAccessBlobPolicy.setSharedAccessExpiryTime(Date.from(expirationTime));
+        return sharedAccessBlobPolicy;
+    }
+
+    @NotNull
+    private static SharedAccessBlobPolicy policy(EnumSet<SharedAccessBlobPermissions> permissions) {
+        return policy(permissions, Instant.now().plus(Duration.ofDays(7)));
+    }
+
+    private static void assertReadAccessGranted(SegmentNodeStorePersistence persistence, Set<String> expectedBlobs) throws Exception {
+        CloudBlobContainer container = getContainerFrom(persistence);
+        Set<String> actualBlobNames = StreamSupport.stream(container.listBlobs().spliterator(), false)
+            .map(blob -> blob.getUri().getPath())
+            .map(path -> path.substring(path.lastIndexOf('/') + 1))
+            .collect(toSet());
+        Set<String> expectedBlobNames = expectedBlobs.stream().map(name -> name + ".txt").collect(toSet());
+
+        assertEquals(expectedBlobNames, actualBlobNames);
+
+        Set<String> actualBlobContent = actualBlobNames.stream()
+            .map(name -> {
+                try {
+                    return container.getBlockBlobReference(name).downloadText();
+                } catch (StorageException | IOException | URISyntaxException e) {
+                    throw new RuntimeException("Error while reading blob " + name, e);
+                }
+            })
+            .collect(toSet());
+        assertEquals(expectedBlobs, actualBlobContent);
+    }
+
+    private static void assertWriteAccessGranted(SegmentNodeStorePersistence persistence) throws Exception {
+        getContainerFrom(persistence)
+            .getBlockBlobReference("test.txt").uploadText("test");
+    }
+
+    private static CloudBlobContainer getContainerFrom(SegmentNodeStorePersistence persistence) throws Exception {
+        return ((AzurePersistence) persistence).getSegmentstoreDirectory().getContainer();
+    }
+
+    private static void assertWriteAccessNotGranted(SegmentNodeStorePersistence persistence) {
+        try {
+            assertWriteAccessGranted(persistence);
+            fail("Write access should not be granted, but writing to the storage succeeded.");
+        } catch (Exception e) {
+            // successful
+        }
+    }
+
+    private static void assertReadAccessNotGranted(SegmentNodeStorePersistence persistence) {
+        try {
+            assertReadAccessGranted(persistence, BLOBS);
+            fail("Read access should not be granted, but reading from the storage succeeded.");
+        } catch (Exception e) {
+            // successful
+        }
+    }
+
+    private static Instant yesterday() {
+        return Instant.now().minus(Duration.ofDays(1));
+    }
+    
+    private static ImmutableSet<String> concat(ImmutableSet<String> blobs, String element) {
+        return ImmutableSet.<String>builder().addAll(blobs).add(element).build();
+    }
+
+    private static Configuration getConfigurationWithSharedAccessSignature(String sasToken) {
+        return getConfiguration(sasToken, null, null);
+    }
+
+    private static Configuration getConfigurationWithAccessKey(String accessKey) {
+        return getConfiguration(null, accessKey, null);
+    }
+
+    private static Configuration getConfigurationWithConfigurationURL(String accessKey) {
+        String connectionString = "DefaultEndpointsProtocol=https;"
+            + "BlobEndpoint=" + azurite.getBlobEndpoint() + ';'
+            + "AccountName=" + AzuriteDockerRule.ACCOUNT_NAME + ';'
+            + "AccountKey=" + accessKey + ';';
+        return getConfiguration(null, null, connectionString);
+    }
+
+    @NotNull
+    private static Configuration getConfiguration(String sasToken, String accessKey, String connectionURL) {
+        return new Configuration() {
+            @Override
+            public String accountName() {
+                return AzuriteDockerRule.ACCOUNT_NAME;
+            }
+
+            @Override
+            public String containerName() {
+                return AzureSegmentStoreService.DEFAULT_CONTAINER_NAME;
+            }
+
+            @Override
+            public String accessKey() {
+                return accessKey != null ? accessKey : "";
+            }
+
+            @Override
+            public String rootPath() {
+                return AzureSegmentStoreService.DEFAULT_ROOT_PATH;
+            }
+
+            @Override
+            public String connectionURL() {
+                return connectionURL != null ? connectionURL : "";
+            }
+
+            @Override
+            public String sharedAccessSignature() {
+                return sasToken != null ? sasToken : "";
+            }
+
+            @Override
+            public String blobEndpoint() {
+                return azurite.getBlobEndpoint();
+            }
+
+            @Override
+            public Class<? extends Annotation> annotationType() {
+                return Configuration.class;
+            }
+        };
+    }
+}
diff --git a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzuriteDockerRule.java b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzuriteDockerRule.java
index 283d1cc..231a9ad 100644
--- a/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzuriteDockerRule.java
+++ b/oak-segment-azure/src/test/java/org/apache/jackrabbit/oak/segment/azure/AzuriteDockerRule.java
@@ -20,10 +20,12 @@ import com.arakelian.docker.junit.DockerRule;
 import com.arakelian.docker.junit.model.ImmutableDockerConfig;
 import com.microsoft.azure.storage.CloudStorageAccount;
 import com.microsoft.azure.storage.StorageException;
+import com.microsoft.azure.storage.blob.CloudBlobClient;
 import com.microsoft.azure.storage.blob.CloudBlobContainer;
 import com.spotify.docker.client.DefaultDockerClient;
 import com.spotify.docker.client.auth.FixedRegistryAuthSupplier;
 
+import org.jetbrains.annotations.NotNull;
 import org.junit.Assume;
 import org.junit.rules.TestRule;
 import org.junit.runner.Description;
@@ -34,42 +36,57 @@ import java.security.InvalidKeyException;
 
 public class AzuriteDockerRule implements TestRule {
 
-    private static final String IMAGE = "trekawek/azurite";
+    private static final String IMAGE = "mcr.microsoft.com/azure-storage/azurite:3.15.0";
+    
+    public static final String ACCOUNT_KEY = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==";
+    public static final String ACCOUNT_NAME = "devstoreaccount1";
 
     private final DockerRule wrappedRule;
 
     public AzuriteDockerRule() {
         wrappedRule = new DockerRule(ImmutableDockerConfig.builder()
-                .image(IMAGE)
-                .name("oak-test-azurite")
-                .ports("10000")
-                .addStartedListener(container -> {
-                    container.waitForPort("10000/tcp");
-                    container.waitForLog("Azure Blob Storage Emulator listening on port 10000");
-                })
-                .addContainerConfigurer(builder -> builder.env("executable=blob"))
-                .alwaysRemoveContainer(true)
-                .build());
-
+            .image(IMAGE)
+            .name("oak-test-azurite")
+            .ports("10000")
+            .addStartedListener(container -> {
+                container.waitForPort("10000/tcp");
+                container.waitForLog("Azurite Blob service is successfully listening at http://0.0.0.0:10000");
+            })
+            .addContainerConfigurer(builder -> builder.env("executable=blob"))
+            .alwaysRemoveContainer(true)
+            .build());
     }
 
     public CloudBlobContainer getContainer(String name) throws URISyntaxException, StorageException, InvalidKeyException {
-        int mappedPort = getMappedPort();
-        CloudStorageAccount cloud = CloudStorageAccount.parse("DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:" + mappedPort + "/devstoreaccount1;");
-        CloudBlobContainer container = cloud.createCloudBlobClient().getContainerReference(name);
+        CloudStorageAccount cloud = getCloudStorageAccount();
+        CloudBlobClient cloudBlobClient = cloud.createCloudBlobClient();
+        CloudBlobContainer container = cloudBlobClient.getContainerReference(name);
         container.deleteIfExists();
         container.create();
         return container;
     }
 
+    public CloudStorageAccount getCloudStorageAccount() throws URISyntaxException, InvalidKeyException {
+        String blobEndpoint = "BlobEndpoint=" + getBlobEndpoint();
+        String accountName = "AccountName=" + ACCOUNT_NAME;
+        String accountKey = "AccountKey=" + ACCOUNT_KEY;
+        return CloudStorageAccount.parse("DefaultEndpointsProtocol=http;" + ";" + accountName + ";" + accountKey + ";" + blobEndpoint);
+    }
+
+    @NotNull
+    public String getBlobEndpoint() {
+        int mappedPort = getMappedPort();
+        return "http://127.0.0.1:" + mappedPort + "/devstoreaccount1";
+    }
+
     @Override
     public Statement apply(Statement statement, Description description) {
         try {
             DefaultDockerClient client = DefaultDockerClient.fromEnv()
-                    .connectTimeoutMillis(5000L)
-                    .readTimeoutMillis(20000L)
-                    .registryAuthSupplier(new FixedRegistryAuthSupplier())
-                    .build();
+                .connectTimeoutMillis(5000L)
+                .readTimeoutMillis(20000L)
+                .registryAuthSupplier(new FixedRegistryAuthSupplier())
+                .build();
             client.ping();
             client.pull(IMAGE);
             client.close();