Skip to content

Commit

Permalink
[BT-698] first pass on BlobTokenGenerator with E2E test (#6824)
Browse files Browse the repository at this point in the history
* first pass on BlobTokenGenerator with E2E test

* update BlobPathBuilder constructor args in test

* account -> container level client
  • Loading branch information
mspector authored Aug 11, 2022
1 parent 890fd1d commit 19d4fdb
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,12 @@ object BlobPathBuilder {
}
}

class BlobPathBuilder(credential: AzureSasCredential, container: String, endpoint: String) extends PathBuilder {
class BlobPathBuilder(blobTokenGenerator: BlobTokenGenerator, container: String, endpoint: String) extends PathBuilder {

val credential: AzureSasCredential = new AzureSasCredential(blobTokenGenerator.getAccessToken)
val fileSystemConfig: Map[String, Object] = Map((AzureFileSystem.AZURE_STORAGE_SAS_TOKEN_CREDENTIAL, credential),
(AzureFileSystem.AZURE_STORAGE_FILE_STORES, container))
(AzureFileSystem.AZURE_STORAGE_FILE_STORES, container),
(AzureFileSystem.AZURE_STORAGE_SKIP_INITIAL_CONTAINER_CHECK, java.lang.Boolean.TRUE))

def retrieveFilesystem(uri: URI): Try[FileSystem] = {
Try(FileSystems.getFileSystem(uri)) recover {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,115 @@
package cromwell.filesystems.blob

import akka.actor.ActorSystem
import com.azure.core.credential.AzureSasCredential
import com.azure.core.management.AzureEnvironment
import com.azure.core.management.profile.AzureProfile
import com.azure.identity.DefaultAzureCredentialBuilder
import com.azure.resourcemanager.AzureResourceManager
import com.azure.storage.blob.BlobContainerClientBuilder
import com.azure.storage.blob.sas.{BlobContainerSasPermission, BlobServiceSasSignatureValues}
import com.azure.storage.common.StorageSharedKeyCredential
import com.typesafe.config.Config
import cromwell.core.WorkflowOptions
import cromwell.core.path.PathBuilderFactory
import cromwell.filesystems.blob.BlobPathBuilder
import net.ceedubs.ficus.Ficus._

import java.time.OffsetDateTime
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.jdk.CollectionConverters._

final case class BlobFileSystemConfig(config: Config)
final case class BlobPathBuilderFactory(globalConfig: Config, instanceConfig: Config, singletonConfig: BlobFileSystemConfig) extends PathBuilderFactory {
val sasToken: String = instanceConfig.as[String]("sas-token")
val container: String = instanceConfig.as[String]("store")
val endpoint: String = instanceConfig.as[String]("endpoint")
val workspaceId: String = instanceConfig.as[String]("workspace-id")
val workspaceManagerURL = singletonConfig.config.as[String]("workspace-manager-url")
val workspaceManagerURL: String = singletonConfig.config.as[String]("workspace-manager-url")

val blobTokenGenerator: BlobTokenGenerator = BlobTokenGenerator.createBlobTokenGenerator(
container, endpoint, Option(workspaceId), Option(workspaceManagerURL))

override def withOptions(options: WorkflowOptions)(implicit as: ActorSystem, ec: ExecutionContext): Future[BlobPathBuilder] = {
Future {
new BlobPathBuilder(new AzureSasCredential(sasToken), container, endpoint)
new BlobPathBuilder(blobTokenGenerator, container, endpoint)
}
}
}

sealed trait BlobTokenGenerator {
def getAccessToken: String
}

object BlobTokenGenerator {
def createBlobTokenGenerator(container: String, endpoint: String): BlobTokenGenerator = {
createBlobTokenGenerator(container, endpoint, None, None)
}
def createBlobTokenGenerator(container: String, endpoint: String, workspaceId: Option[String], workspaceManagerURL: Option[String]): BlobTokenGenerator = {
(container: String, endpoint: String, workspaceId, workspaceManagerURL) match {
case (container, endpoint, None, None) =>
NativeBlobTokenGenerator(container, endpoint)
case (container, endpoint, Some(workspaceId), Some(workspaceManagerURL)) =>
WSMBlobTokenGenerator(container, endpoint, workspaceId, workspaceManagerURL)
case _ =>
throw new Exception("Arguments provided do not match any available BlobTokenGenerator implementation.")
}
}
}

case class WSMBlobTokenGenerator(container: String, endpoint: String, workspaceId: String, workspaceManagerURL: String) extends BlobTokenGenerator {
def getAccessToken: String = {
throw new NotImplementedError
}
}

case class NativeBlobTokenGenerator(container: String, endpoint: String) extends BlobTokenGenerator {
def getAccessToken: String = {
val storageAccountName = BlobPathBuilder.parseStorageAccount(BlobPathBuilder.parseURI(endpoint)) match {
case Some(storageAccountName) => storageAccountName
case _ => throw new Exception("Storage account could not be parsed from endpoint")
}

val profile = new AzureProfile(AzureEnvironment.AZURE)
val azureCredential = new DefaultAzureCredentialBuilder()
.authorityHost(profile.getEnvironment.getActiveDirectoryEndpoint)
.build
val azure = AzureResourceManager.authenticate(azureCredential, profile).withDefaultSubscription

val storageAccounts = azure.storageAccounts()
val storageAccount = storageAccounts
.list()
.asScala
.find(_.name == storageAccountName)

val storageAccountKeys = storageAccount match {
case Some(value) => value.getKeys.asScala.map(_.value())
case _ => throw new Exception("Storage Account not found")
}

val storageAccountKey = storageAccountKeys.headOption match {
case Some(value) => value
case _ => throw new Exception("Storage Account has no keys")
}

val keyCredential = new StorageSharedKeyCredential(
storageAccountName,
storageAccountKey
)
val blobContainerClient = new BlobContainerClientBuilder()
.credential(keyCredential)
.endpoint(endpoint)
.containerName(container)
.buildClient()

val blobContainerSasPermission = new BlobContainerSasPermission()
.setReadPermission(true)
.setCreatePermission(true)
.setListPermission(true)
val blobServiceSasSignatureValues = new BlobServiceSasSignatureValues(
OffsetDateTime.now.plusDays(1),
blobContainerSasPermission
)

blobContainerClient.generateSas(blobServiceSasSignatureValues)
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
package cromwell.filesystems.blob

import com.azure.core.credential.AzureSasCredential
import cromwell.filesystems.blob.BlobPathBuilder
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

import java.nio.file.Files

object BlobPathBuilderSpec {
Expand Down Expand Up @@ -47,17 +43,19 @@ class BlobPathBuilderSpec extends AnyFlatSpec with Matchers{
}

ignore should "build a blob path from a test string and read a file" in {
val endpoint = BlobPathBuilderSpec.buildEndpoint("teststorageaccount")
val endpoint = BlobPathBuilderSpec.buildEndpoint("coaexternalstorage")
val endpointHost = BlobPathBuilder.parseURI(endpoint).getHost
val store = "testContainer"
val evalPath = "/test/file.txt"
val sas = "{SAS TOKEN HERE}"
val store = "inputs"
val evalPath = "/test/inputFile.txt"
val blobTokenGenerator: BlobTokenGenerator = BlobTokenGenerator.createBlobTokenGenerator(store, endpoint)
val testString = endpoint + "/" + store + evalPath
val blobPath: BlobPath = new BlobPathBuilder(new AzureSasCredential(sas), store, endpoint) build testString getOrElse fail()
val blobPath: BlobPath = new BlobPathBuilder(blobTokenGenerator, store, endpoint) build testString getOrElse fail()

blobPath.container should equal(store)
blobPath.endpoint should equal(endpoint)
blobPath.pathAsString should equal(testString)
blobPath.pathWithoutScheme should equal(endpointHost + "/" + store + evalPath)

val is = Files.newInputStream(blobPath.nioPath)
val fileText = (is.readAllBytes.map(_.toChar)).mkString
fileText should include ("This is my test file!!!! Did it work?")
Expand All @@ -67,10 +65,10 @@ class BlobPathBuilderSpec extends AnyFlatSpec with Matchers{
val endpoint = BlobPathBuilderSpec.buildEndpoint("coaexternalstorage")
val store = "inputs"
val evalPath = "/test/inputFile.txt"
val sas = "{SAS TOKEN HERE}"
val blobTokenGenerator: BlobTokenGenerator = BlobTokenGenerator.createBlobTokenGenerator(store, endpoint)
val testString = endpoint + "/" + store + evalPath
val blobPath1: BlobPath = new BlobPathBuilder(new AzureSasCredential(sas), store, endpoint) build testString getOrElse fail()
val blobPath2: BlobPath = new BlobPathBuilder(new AzureSasCredential(sas), store, endpoint) build testString getOrElse fail()
val blobPath1: BlobPath = new BlobPathBuilder(blobTokenGenerator, store, endpoint) build testString getOrElse fail()
val blobPath2: BlobPath = new BlobPathBuilder(blobTokenGenerator, store, endpoint) build testString getOrElse fail()
blobPath1 should equal(blobPath2)
}
}
4 changes: 3 additions & 1 deletion project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,9 @@ object Dependencies {
exclude("jakarta.activation", "jakarta.activation-api"),
"com.azure" % "azure-security-keyvault-secrets" % azureKeyVaultSdkV
exclude("jakarta.xml.bind", "jakarta.xml.bind-api")
exclude("jakarta.activation", "jakarta.activation-api")
exclude("jakarta.activation", "jakarta.activation-api"),
"com.azure" % "azure-core-management" % "1.7.0",
"com.azure.resourcemanager" % "azure-resourcemanager" % "2.17.0"
)

val implFtpDependencies = List(
Expand Down

0 comments on commit 19d4fdb

Please sign in to comment.