Implement the containers function for C# (#2078)

Fairly straightforward but required implementation of a few more functions on the `IContainers`/`Containers` class.
This commit is contained in:
George Pollard
2022-07-13 10:00:22 +12:00
committed by GitHub
parent 9d5d86a03b
commit ec515a57fd
10 changed files with 457 additions and 86 deletions

View File

@ -0,0 +1,117 @@
using Azure.Storage.Sas;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
namespace Microsoft.OneFuzz.Service;
public class ContainersFunction {
private readonly ILogTracer _logger;
private readonly IEndpointAuthorization _auth;
private readonly IOnefuzzContext _context;
public ContainersFunction(ILogTracer logger, IEndpointAuthorization auth, IOnefuzzContext context) {
_logger = logger;
_auth = auth;
_context = context;
}
// [Function("Download")]
public Async.Task<HttpResponseData> Run([HttpTrigger("GET", "POST", "DELETE")] HttpRequestData req)
=> _auth.CallIfUser(req, r => r.Method switch {
"GET" => Get(r),
"POST" => Post(r),
"DELETE" => Delete(r),
_ => throw new NotSupportedException(),
});
private async Async.Task<HttpResponseData> Get(HttpRequestData req) {
// see if one particular container is specified:
if (req.Body.Length > 0) {
var request = await RequestHandling.ParseRequest<ContainerGet>(req);
if (!request.IsOk) {
return await _context.RequestHandling.NotOk(req, request.ErrorV, "container get");
}
var get = request.OkV;
var container = await _context.Containers.FindContainer(get.Name, StorageType.Corpus);
if (container is null) {
return await _context.RequestHandling.NotOk(
req,
new Error(
Code: ErrorCode.INVALID_REQUEST,
Errors: new[] { "invalid container" }),
context: get.Name.ContainerName);
}
var metadata = (await container.GetPropertiesAsync()).Value.Metadata;
var sas = await _context.Containers.GetContainerSasUrl(
get.Name,
StorageType.Corpus,
BlobContainerSasPermissions.Read
| BlobContainerSasPermissions.Write
| BlobContainerSasPermissions.Delete
| BlobContainerSasPermissions.List);
return await RequestHandling.Ok(req, new ContainerInfo(
Name: get.Name,
SasUrl: sas,
Metadata: metadata));
}
// otherwise list all containers
var containers = await _context.Containers.GetContainers(StorageType.Corpus);
var result = containers.Select(c => new ContainerInfoBase(new Container(c.Key), c.Value));
return await RequestHandling.Ok(req, result);
}
private async Async.Task<HttpResponseData> Delete(HttpRequestData req) {
var request = await RequestHandling.ParseRequest<ContainerDelete>(req);
if (!request.IsOk) {
return await _context.RequestHandling.NotOk(req, request.ErrorV, context: "container delete");
}
var delete = request.OkV;
_logger.Info($"container - deleting {delete.Name}");
var container = await _context.Containers.FindContainer(delete.Name, StorageType.Corpus);
var deleted = false;
if (container is not null) {
deleted = await container.DeleteIfExistsAsync();
}
return await RequestHandling.Ok(req, deleted);
}
private async Async.Task<HttpResponseData> Post(HttpRequestData req) {
var request = await RequestHandling.ParseRequest<ContainerCreate>(req);
if (!request.IsOk) {
return await _context.RequestHandling.NotOk(req, request.ErrorV, context: "container create");
}
var post = request.OkV;
_logger.Info($"container - creating {post.Name}");
var sas = await _context.Containers.CreateContainer(
post.Name,
StorageType.Corpus,
post.Metadata);
if (sas is null) {
return await _context.RequestHandling.NotOk(
req,
new Error(
Code: ErrorCode.INVALID_REQUEST,
Errors: new[] { "invalid container" }),
context: post.Name.ContainerName);
}
return await RequestHandling.Ok(
req,
new ContainerInfo(
Name: post.Name,
SasUrl: sas,
Metadata: post.Metadata));
}
}

View File

@ -95,3 +95,17 @@ public record ExitStatus(
int? Code,
int? Signal,
bool Success);
public record ContainerGet(
Container Name
) : BaseRequest;
public record ContainerCreate(
Container Name,
IDictionary<string, string>? Metadata = null
) : BaseRequest;
public record ContainerDelete(
Container Name,
IDictionary<string, string>? Metadata = null
) : BaseRequest;

View File

@ -54,13 +54,23 @@ public record InfoVersion(
string Build,
string Version);
public record AgentRegistrationResponse(
Uri EventsUrl,
Uri WorkQueue,
Uri CommandsUrl
) : BaseResponse();
public record ContainerInfoBase(
Container Name,
IDictionary<string, string>? Metadata
) : BaseResponse();
public record ContainerInfo(
Container Name,
IDictionary<string, string>? Metadata,
Uri SasUrl
) : BaseResponse();
public class BaseResponseConverter : JsonConverter<BaseResponse> {
public override BaseResponse? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
return null;

View File

@ -1,8 +1,10 @@
using System.Threading;
using System.Threading.Tasks;
using Azure;
using Azure.ResourceManager;
using Azure.Storage;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Azure.Storage.Sas;
namespace Microsoft.OneFuzz.Service;
@ -11,6 +13,10 @@ namespace Microsoft.OneFuzz.Service;
public interface IContainers {
public Async.Task<BinaryData?> GetBlob(Container container, string name, StorageType storageType);
public Async.Task<Uri?> CreateContainer(Container container, StorageType storageType, IDictionary<string, string>? metadata);
public Async.Task<BlobContainerClient?> GetOrCreateContainerClient(Container container, StorageType storageType, IDictionary<string, string>? metadata);
public Async.Task<BlobContainerClient?> FindContainer(Container container, StorageType storageType);
public Async.Task<Uri> GetFileSasUrl(Container container, string name, StorageType storageType, BlobSasPermissions permissions, TimeSpan? duration = null);
@ -24,9 +30,9 @@ public interface IContainers {
public Async.Task<bool> BlobExists(Container container, string name, StorageType storageType);
public Async.Task<Uri> AddContainerSasUrl(Uri uri, TimeSpan? duration = null);
public Async.Task<Dictionary<string, IDictionary<string, string>>> GetContainers(StorageType corpus);
}
public class Containers : IContainers {
private ILogTracer _log;
private IStorage _storage;
@ -76,6 +82,44 @@ public class Containers : IContainers {
}
}
public async Task<Uri?> CreateContainer(Container container, StorageType storageType, IDictionary<string, string>? metadata) {
var client = await GetOrCreateContainerClient(container, storageType, metadata);
if (client is null) {
return null;
}
return GetContainerSasUrlService(client, _containerCreatePermissions);
}
private static readonly BlobContainerSasPermissions _containerCreatePermissions
= BlobContainerSasPermissions.Read
| BlobContainerSasPermissions.Write
| BlobContainerSasPermissions.Delete
| BlobContainerSasPermissions.List;
public async Task<BlobContainerClient?> GetOrCreateContainerClient(Container container, StorageType storageType, IDictionary<string, string>? metadata) {
var containerClient = await FindContainer(container, StorageType.Corpus);
if (containerClient is not null) {
return containerClient;
}
var account = _storage.ChooseAccount(storageType);
var client = await _storage.GetBlobServiceClientForAccount(account);
var containerName = _config.OneFuzzStoragePrefix + container.ContainerName;
var cc = client.GetBlobContainerClient(containerName);
try {
await cc.CreateAsync(metadata: metadata);
} catch (RequestFailedException ex) when (ex.ErrorCode == "ContainerAlreadyExists") {
// note: resource exists error happens during creation if the container
// is being deleted
_log.Error($"unable to create container. account: {account} container: {container.ContainerName} metadata: {metadata} - {ex.Message}");
return null;
}
return cc;
}
public async Async.Task<BlobContainerClient?> FindContainer(Container container, StorageType storageType) {
// # check secondary accounts first by searching in reverse.
// #
@ -87,31 +131,20 @@ public class Containers : IContainers {
var containerName = _config.OneFuzzStoragePrefix + container.ContainerName;
var containers = _storage.GetAccounts(storageType).AsEnumerable()
var containers =
_storage.GetAccounts(storageType)
.Reverse()
.Select(async account => (await GetBlobService(account))?.GetBlobContainerClient(containerName));
.Select(async account => (await _storage.GetBlobServiceClientForAccount(account)).GetBlobContainerClient(containerName));
foreach (var c in containers) {
var client = await c;
if (client != null && (await client.ExistsAsync()).Value) {
if ((await client.ExistsAsync()).Value) {
return client;
}
}
return null;
}
private async Async.Task<BlobServiceClient?> GetBlobService(string accountId) {
_log.Info($"getting blob container (account_id: {accountId})");
var (accountName, accountKey) = await _storage.GetStorageAccountNameAndKey(accountId);
if (accountName == null) {
_log.Error("Failed to get storage account name");
return null;
}
var storageKeyCredential = new StorageSharedKeyCredential(accountName, accountKey);
var accountUrl = _storage.GetBlobEndpoint(accountName);
return new BlobServiceClient(accountUrl, storageKeyCredential);
}
public async Async.Task<Uri> GetFileSasUrl(Container container, string name, StorageType storageType, BlobSasPermissions permissions, TimeSpan? duration = null) {
var client = await FindContainer(container, storageType) ?? throw new Exception($"unable to find container: {container.ContainerName} - {storageType}");
@ -153,13 +186,12 @@ public class Containers : IContainers {
public static Uri? GetContainerSasUrlService(
BlobContainerClient client,
BlobSasPermissions permissions,
BlobContainerSasPermissions permissions,
bool tag = false,
TimeSpan? timeSpan = null) {
var (start, expiry) = SasTimeWindow(timeSpan ?? TimeSpan.FromDays(30.0));
var sasBuilder = new BlobSasBuilder(permissions, expiry) { StartsOn = start };
var sas = client.GenerateSasUri(sasBuilder);
return sas;
return client.GenerateSasUri(sasBuilder);
}
public async Async.Task<Uri> AddContainerSasUrl(Uri uri, TimeSpan? duration = null) {
@ -188,7 +220,7 @@ public class Containers : IContainers {
var (startTime, endTime) = SasTimeWindow(duration ?? CONTAINER_SAS_DEFAULT_DURATION);
var sasBuilder = new BlobSasBuilder(permissions, endTime) {
StartsOn = startTime,
BlobContainerName = container.ContainerName
BlobContainerName = _config.OneFuzzStoragePrefix + container.ContainerName,
};
var sasUrl = client.GenerateSasUri(sasBuilder);
@ -199,4 +231,20 @@ public class Containers : IContainers {
var client = await FindContainer(container, storageType) ?? throw new Exception($"unable to find container: {container.ContainerName} - {storageType}");
return await client.GetBlobClient(name).ExistsAsync();
}
public async Task<Dictionary<string, IDictionary<string, string>>> GetContainers(StorageType corpus) {
var accounts = _storage.GetAccounts(corpus);
IEnumerable<IEnumerable<KeyValuePair<string, IDictionary<string, string>>>> data =
await Async.Task.WhenAll(accounts.Select(async acc => {
var service = await _storage.GetBlobServiceClientForAccount(acc);
if (service is null) {
throw new InvalidOperationException($"unable to get blob service for account {acc}");
}
return await service.GetBlobContainersAsync(BlobContainerTraits.Metadata).Select(container =>
KeyValuePair.Create(container.Name, container.Properties.Metadata)).ToListAsync();
}));
return new(data.SelectMany(x => x));
}
}

View File

@ -1,7 +1,10 @@
using System.Text.Json;
using Azure.Core;
using Azure.Data.Tables;
using Azure.ResourceManager;
using Azure.ResourceManager.Storage;
using Azure.Storage;
using Azure.Storage.Blobs;
using Microsoft.Extensions.Caching.Memory;
namespace Microsoft.OneFuzz.Service;
@ -12,8 +15,9 @@ public enum StorageType {
}
public interface IStorage {
public IEnumerable<string> CorpusAccounts();
string GetPrimaryAccount(StorageType storageType);
public IReadOnlyList<string> CorpusAccounts();
public string GetPrimaryAccount(StorageType storageType);
public IReadOnlyList<string> GetAccounts(StorageType storageType);
public Uri GetTableEndpoint(string accountId);
@ -25,7 +29,39 @@ public interface IStorage {
public Async.Task<string?> GetStorageAccountNameKeyByName(string accountName);
public IEnumerable<string> GetAccounts(StorageType storageType);
/// Picks either the single primary account or a random secondary account.
public string ChooseAccount(StorageType storageType) {
var accounts = GetAccounts(storageType);
if (!accounts.Any()) {
throw new InvalidOperationException($"no storage accounts for {storageType}");
}
if (accounts.Count == 1) {
return accounts[0];
}
// Use a random secondary storage account if any are available. This
// reduces IOP contention for the Storage Queues, which are only available
// on primary accounts
//
// security note: this is not used as a security feature
var secondaryAccounts = accounts.Skip(1).ToList();
return secondaryAccounts[Random.Shared.Next(secondaryAccounts.Count)];
}
public async Async.Task<BlobServiceClient> GetBlobServiceClientForAccount(string accountId) {
var (accountName, accountKey) = await GetStorageAccountNameAndKey(accountId);
var storageKeyCredential = new StorageSharedKeyCredential(accountName, accountKey);
var accountUrl = GetBlobEndpoint(accountName);
return new BlobServiceClient(accountUrl, storageKeyCredential);
}
public async Async.Task<TableServiceClient> GetTableServiceClientForAccount(string accountId) {
var (accountName, accountKey) = await GetStorageAccountNameAndKey(accountId);
var storageKeyCredential = new TableSharedKeyCredential(accountName, accountKey);
var accountUrl = GetTableEndpoint(accountName);
return new TableServiceClient(accountUrl, storageKeyCredential);
}
}
public sealed class Storage : IStorage, IDisposable {
@ -59,8 +95,8 @@ public sealed class Storage : IStorage, IDisposable {
return _armClient;
}
public IEnumerable<string> CorpusAccounts() {
return _cache.GetOrCreate<List<string>>("CorpusAccounts", cacheEntry => {
public IReadOnlyList<string> CorpusAccounts() {
return _cache.GetOrCreate<IReadOnlyList<string>>("CorpusAccounts", cacheEntry => {
var skip = GetFuncStorage();
var results = new List<string> { GetFuzzStorage() };
@ -127,29 +163,8 @@ public sealed class Storage : IStorage, IDisposable {
});
}
public string ChooseAccounts(StorageType storageType) {
var accounts = GetAccounts(storageType);
if (!accounts.Any()) {
throw new Exception($"No Storage Accounts for {storageType}");
}
var account_list = accounts.ToList();
if (account_list.Count == 1) {
return account_list[0];
}
// Use a random secondary storage account if any are available. This
// reduces IOP contention for the Storage Queues, which are only available
// on primary accounts
//
// security note: this is not used as a security feature
var random = new Random();
var index = random.Next(account_list.Count);
return account_list[index]; // nosec
}
public IEnumerable<string> GetAccounts(StorageType storageType) {
public IReadOnlyList<string> GetAccounts(StorageType storageType) {
switch (storageType) {
case StorageType.Corpus:
return CorpusAccounts();

View File

@ -98,9 +98,7 @@ namespace ApiService.OneFuzzLib.Orm {
var tableName = _context.ServiceConfiguration.OneFuzzStoragePrefix + table;
var account = accountId ?? _context.ServiceConfiguration.OneFuzzFuncStorage ?? throw new ArgumentNullException(nameof(accountId));
var (name, key) = await _context.Storage.GetStorageAccountNameAndKey(account);
var endpoint = _context.Storage.GetTableEndpoint(name);
var tableClient = new TableServiceClient(endpoint, new TableSharedKeyCredential(name, key));
var tableClient = await _context.Storage.GetTableServiceClientForAccount(account);
await tableClient.CreateTableIfNotExistsAsync(tableName);
return tableClient.GetTableClient(tableName);
}

View File

@ -0,0 +1,175 @@

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using Azure.Storage.Blobs;
using IntegrationTests.Fakes;
using Microsoft.OneFuzz.Service;
using Xunit;
using Xunit.Abstractions;
using Async = System.Threading.Tasks;
namespace IntegrationTests;
[Trait("Category", "Live")]
public class AzureStorageContainersTest : ContainersTestBase {
public AzureStorageContainersTest(ITestOutputHelper output)
: base(output, Integration.AzureStorage.FromEnvironment()) { }
}
public class AzuriteContainersTest : ContainersTestBase {
public AzuriteContainersTest(ITestOutputHelper output)
: base(output, new Integration.AzuriteStorage()) { }
}
public abstract class ContainersTestBase : FunctionTestBase {
public ContainersTestBase(ITestOutputHelper output, IStorage storage)
: base(output, storage) { }
[Theory]
[InlineData("GET")]
[InlineData("POST")]
[InlineData("DELETE")]
public async Async.Task WithoutAuthorization_IsRejected(string method) {
var auth = new TestEndpointAuthorization(RequestType.NoAuthorization, Logger, Context);
var func = new ContainersFunction(Logger, auth, Context);
var result = await func.Run(TestHttpRequestData.Empty(method));
Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode);
var err = BodyAs<Error>(result);
Assert.Equal(ErrorCode.UNAUTHORIZED, err.Code);
}
[Fact]
public async Async.Task CanDelete() {
var containerName = "test";
var client = GetContainerClient(containerName);
await client.CreateIfNotExistsAsync();
var msg = TestHttpRequestData.FromJson("DELETE", new ContainerDelete(new Container(containerName)));
var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context);
var func = new ContainersFunction(Logger, auth, Context);
var result = await func.Run(msg);
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
// container should be gone
Assert.False(await client.ExistsAsync());
}
[Fact]
public async Async.Task CanPost_New() {
var meta = new Dictionary<string, string> { { "some", "value" } };
var containerName = "test";
var msg = TestHttpRequestData.FromJson("POST", new ContainerCreate(new Container(containerName), meta));
var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context);
var func = new ContainersFunction(Logger, auth, Context);
var result = await func.Run(msg);
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
// container should be created with metadata:
var client = GetContainerClient(containerName);
Assert.True(await client.ExistsAsync());
var props = await client.GetPropertiesAsync();
Assert.Equal(meta, props.Value.Metadata);
var response = BodyAs<ContainerInfo>(result);
await AssertCanCRUD(response.SasUrl);
}
[Fact]
public async Async.Task CanPost_Existing() {
var containerName = "test";
var client = GetContainerClient(containerName);
await client.CreateIfNotExistsAsync();
var metadata = new Dictionary<string, string> { { "some", "value" } };
var msg = TestHttpRequestData.FromJson("POST", new ContainerCreate(new Container(containerName), metadata));
var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context);
var func = new ContainersFunction(Logger, auth, Context);
var result = await func.Run(msg);
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
// metadata should _not_ be updated:
var props = await client.GetPropertiesAsync();
Assert.Empty(props.Value.Metadata);
var response = BodyAs<ContainerInfo>(result);
await AssertCanCRUD(response.SasUrl);
}
[Fact]
public async Async.Task Get_Existing() {
var containerName = "test";
{
var client = GetContainerClient(containerName);
await client.CreateIfNotExistsAsync();
}
var msg = TestHttpRequestData.FromJson("GET", new ContainerGet(new Container(containerName)));
var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context);
var func = new ContainersFunction(Logger, auth, Context);
var result = await func.Run(msg);
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
// we should get back a SAS URI that works (create, delete, list, read):
var info = BodyAs<ContainerInfo>(result);
await AssertCanCRUD(info.SasUrl);
}
[Fact]
public async Async.Task Get_Missing_Fails() {
var msg = TestHttpRequestData.FromJson("GET", new ContainerGet(new Container("container")));
var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context);
var func = new ContainersFunction(Logger, auth, Context);
var result = await func.Run(msg);
Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode);
}
[Fact]
public async Async.Task List_Existing() {
var meta1 = new Dictionary<string, string> { { "key1", "value1" } };
var meta2 = new Dictionary<string, string> { { "key2", "value2" } };
await GetContainerClient("one").CreateIfNotExistsAsync(metadata: meta1);
await GetContainerClient("two").CreateIfNotExistsAsync(metadata: meta2);
var msg = TestHttpRequestData.Empty("GET"); // this means list all
var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context);
var func = new ContainersFunction(Logger, auth, Context);
var result = await func.Run(msg);
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
var list = BodyAs<ContainerInfoBase[]>(result);
// other tests can run in parallel, so filter to just our containers:
var cs = list.Where(ci => ci.Name.ContainerName.StartsWith(Context.ServiceConfiguration.OneFuzzStoragePrefix)).ToList();
Assert.Equal(2, cs.Count);
// ensure correct metadata was returned.
// these will be in order as "one"<"two"
Assert.Equal(meta1, cs[0].Metadata);
Assert.Equal(meta2, cs[1].Metadata);
}
private static async Async.Task AssertCanCRUD(Uri sasUrl) {
var client = new BlobContainerClient(sasUrl);
await client.UploadBlobAsync("blob", new BinaryData("content")); // create
var b = Assert.Single(await client.GetBlobsAsync().ToListAsync()); // list
using (var s = await client.GetBlobClient(b.Name).OpenReadAsync())
using (var sr = new StreamReader(s)) {
Assert.Equal("content", await sr.ReadToEndAsync()); // read
}
await client.DeleteBlobAsync("blob"); // delete
}
}

View File

@ -33,16 +33,8 @@ sealed class AzureStorage : IStorage {
AccountKey = accountKey;
}
public IEnumerable<string> CorpusAccounts() {
throw new System.NotImplementedException();
}
public IEnumerable<string> GetAccounts(StorageType storageType) {
yield return AccountName;
}
public string GetPrimaryAccount(StorageType storageType) {
throw new System.NotImplementedException();
public IReadOnlyList<string> GetAccounts(StorageType storageType) {
return new[] { AccountName };
}
public Task<(string, string)> GetStorageAccountNameAndKey(string accountId)
@ -61,4 +53,19 @@ sealed class AzureStorage : IStorage {
public Uri GetBlobEndpoint(string accountId)
=> new($"https://{AccountName}.blob.core.windows.net/");
IReadOnlyList<string> IStorage.CorpusAccounts() {
throw new NotImplementedException();
}
public IReadOnlyList<string> CorpusAccounts() {
throw new System.NotImplementedException();
}
public string GetPrimaryAccount(StorageType storageType) {
throw new System.NotImplementedException();
}
public Task<string?> GetStorageAccountNameAndKeyByName(string accountName) {
throw new System.NotImplementedException();
}
}

View File

@ -37,17 +37,17 @@ sealed class AzuriteStorage : IStorage {
public Task<(string, string)> GetStorageAccountNameAndKey(string accountId)
=> Async.Task.FromResult((AccountName, AccountKey));
public Task<string?> GetStorageAccountNameKeyByName(string accountName) {
return Async.Task.FromResult(AccountName)!;
public IReadOnlyList<string> GetAccounts(StorageType storageType) {
return new[] { AccountName };
}
public IEnumerable<string> CorpusAccounts() {
public IReadOnlyList<string> CorpusAccounts() {
throw new System.NotImplementedException();
}
public string GetPrimaryAccount(StorageType storageType) => AccountName;
public IEnumerable<string> GetAccounts(StorageType storageType) {
yield return AccountName;
public Task<string?> GetStorageAccountNameKeyByName(string accountName) {
throw new NotImplementedException();
}
}

View File

@ -3,7 +3,6 @@ using System.IO;
using System.Linq;
using ApiService.OneFuzzLib.Orm;
using Azure.Data.Tables;
using Azure.Storage;
using Azure.Storage.Blobs;
using IntegrationTests.Fakes;
using Microsoft.Azure.Functions.Worker.Http;
@ -50,9 +49,7 @@ public abstract class FunctionTestBase : IDisposable {
Context = new TestContext(Logger, _storage, creds, _storagePrefix);
// set up blob client for test purposes:
var (accountName, accountKey) = _storage.GetStorageAccountNameAndKey("").Result; // for test impls this is always sync
var endpoint = _storage.GetBlobEndpoint("");
_blobClient = new BlobServiceClient(endpoint, new StorageSharedKeyCredential(accountName, accountKey));
_blobClient = _storage.GetBlobServiceClientForAccount("").Result; // for test implementations this is always sync
}
protected static string BodyAsString(HttpResponseData data) {
@ -67,21 +64,13 @@ public abstract class FunctionTestBase : IDisposable {
public void Dispose() {
GC.SuppressFinalize(this);
var (accountName, accountKey) = _storage.GetStorageAccountNameAndKey("").Result; // sync for test impls
if (accountName is not null && accountKey is not null) {
// clean up any tables & blobs that this test created
CleanupTables(_storage.GetTableEndpoint(accountName),
new TableSharedKeyCredential(accountName, accountKey));
CleanupBlobs(_storage.GetBlobEndpoint(accountName),
new StorageSharedKeyCredential(accountName, accountKey));
}
// clean up any tables & blobs that this test created
// these Get methods are always sync for test impls
CleanupTables(_storage.GetTableServiceClientForAccount("").Result);
CleanupBlobs(_storage.GetBlobServiceClientForAccount("").Result);
}
private void CleanupBlobs(Uri endpoint, StorageSharedKeyCredential creds) {
var blobClient = new BlobServiceClient(endpoint, creds);
private void CleanupBlobs(BlobServiceClient blobClient) {
var containersToDelete = blobClient.GetBlobContainers(prefix: _storagePrefix);
foreach (var container in containersToDelete.Where(c => c.IsDeleted != true)) {
try {
@ -94,9 +83,7 @@ public abstract class FunctionTestBase : IDisposable {
}
}
private void CleanupTables(Uri endpoint, TableSharedKeyCredential creds) {
var tableClient = new TableServiceClient(endpoint, creds);
private void CleanupTables(TableServiceClient tableClient) {
var tablesToDelete = tableClient.Query(filter: Query.StartsWith("TableName", _storagePrefix));
foreach (var table in tablesToDelete) {
try {