diff --git a/src/ApiService/ApiService/Containers.cs b/src/ApiService/ApiService/Containers.cs new file mode 100644 index 000000000..149f68add --- /dev/null +++ b/src/ApiService/ApiService/Containers.cs @@ -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 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 Get(HttpRequestData req) { + + // see if one particular container is specified: + if (req.Body.Length > 0) { + var request = await RequestHandling.ParseRequest(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 Delete(HttpRequestData req) { + var request = await RequestHandling.ParseRequest(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 Post(HttpRequestData req) { + var request = await RequestHandling.ParseRequest(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)); + } +} diff --git a/src/ApiService/ApiService/OneFuzzTypes/Requests.cs b/src/ApiService/ApiService/OneFuzzTypes/Requests.cs index 3ea21e071..9b6a0409b 100644 --- a/src/ApiService/ApiService/OneFuzzTypes/Requests.cs +++ b/src/ApiService/ApiService/OneFuzzTypes/Requests.cs @@ -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? Metadata = null +) : BaseRequest; + +public record ContainerDelete( + Container Name, + IDictionary? Metadata = null +) : BaseRequest; diff --git a/src/ApiService/ApiService/OneFuzzTypes/Responses.cs b/src/ApiService/ApiService/OneFuzzTypes/Responses.cs index 449af4241..a46c4c419 100644 --- a/src/ApiService/ApiService/OneFuzzTypes/Responses.cs +++ b/src/ApiService/ApiService/OneFuzzTypes/Responses.cs @@ -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? Metadata +) : BaseResponse(); + +public record ContainerInfo( + Container Name, + IDictionary? Metadata, + Uri SasUrl +) : BaseResponse(); + public class BaseResponseConverter : JsonConverter { public override BaseResponse? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return null; diff --git a/src/ApiService/ApiService/onefuzzlib/Containers.cs b/src/ApiService/ApiService/onefuzzlib/Containers.cs index 3a6d10868..a620a5b14 100644 --- a/src/ApiService/ApiService/onefuzzlib/Containers.cs +++ b/src/ApiService/ApiService/onefuzzlib/Containers.cs @@ -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 GetBlob(Container container, string name, StorageType storageType); + public Async.Task CreateContainer(Container container, StorageType storageType, IDictionary? metadata); + + public Async.Task GetOrCreateContainerClient(Container container, StorageType storageType, IDictionary? metadata); + public Async.Task FindContainer(Container container, StorageType storageType); public Async.Task GetFileSasUrl(Container container, string name, StorageType storageType, BlobSasPermissions permissions, TimeSpan? duration = null); @@ -24,9 +30,9 @@ public interface IContainers { public Async.Task BlobExists(Container container, string name, StorageType storageType); public Async.Task AddContainerSasUrl(Uri uri, TimeSpan? duration = null); + public Async.Task>> GetContainers(StorageType corpus); } - public class Containers : IContainers { private ILogTracer _log; private IStorage _storage; @@ -76,6 +82,44 @@ public class Containers : IContainers { } } + public async Task CreateContainer(Container container, StorageType storageType, IDictionary? 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 GetOrCreateContainerClient(Container container, StorageType storageType, IDictionary? 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 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 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 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 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>> GetContainers(StorageType corpus) { + var accounts = _storage.GetAccounts(corpus); + IEnumerable>>> 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)); + } } diff --git a/src/ApiService/ApiService/onefuzzlib/Storage.cs b/src/ApiService/ApiService/onefuzzlib/Storage.cs index 7e5d997d5..e225077c6 100644 --- a/src/ApiService/ApiService/onefuzzlib/Storage.cs +++ b/src/ApiService/ApiService/onefuzzlib/Storage.cs @@ -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 CorpusAccounts(); - string GetPrimaryAccount(StorageType storageType); + public IReadOnlyList CorpusAccounts(); + public string GetPrimaryAccount(StorageType storageType); + public IReadOnlyList GetAccounts(StorageType storageType); public Uri GetTableEndpoint(string accountId); @@ -25,7 +29,39 @@ public interface IStorage { public Async.Task GetStorageAccountNameKeyByName(string accountName); - public IEnumerable 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 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 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 CorpusAccounts() { - return _cache.GetOrCreate>("CorpusAccounts", cacheEntry => { + public IReadOnlyList CorpusAccounts() { + return _cache.GetOrCreate>("CorpusAccounts", cacheEntry => { var skip = GetFuncStorage(); var results = new List { 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 GetAccounts(StorageType storageType) { + public IReadOnlyList GetAccounts(StorageType storageType) { switch (storageType) { case StorageType.Corpus: return CorpusAccounts(); diff --git a/src/ApiService/ApiService/onefuzzlib/orm/Orm.cs b/src/ApiService/ApiService/onefuzzlib/orm/Orm.cs index 7555d3397..85f13d908 100644 --- a/src/ApiService/ApiService/onefuzzlib/orm/Orm.cs +++ b/src/ApiService/ApiService/onefuzzlib/orm/Orm.cs @@ -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); } diff --git a/src/ApiService/IntegrationTests/ContainersTests.cs b/src/ApiService/IntegrationTests/ContainersTests.cs new file mode 100644 index 000000000..fa1a92d98 --- /dev/null +++ b/src/ApiService/IntegrationTests/ContainersTests.cs @@ -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(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 { { "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(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 { { "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(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(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 { { "key1", "value1" } }; + var meta2 = new Dictionary { { "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(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 + } +} diff --git a/src/ApiService/IntegrationTests/Integration/AzureStorage.cs b/src/ApiService/IntegrationTests/Integration/AzureStorage.cs index 5b4f917ed..ca0b69a47 100644 --- a/src/ApiService/IntegrationTests/Integration/AzureStorage.cs +++ b/src/ApiService/IntegrationTests/Integration/AzureStorage.cs @@ -33,16 +33,8 @@ sealed class AzureStorage : IStorage { AccountKey = accountKey; } - public IEnumerable CorpusAccounts() { - throw new System.NotImplementedException(); - } - - public IEnumerable GetAccounts(StorageType storageType) { - yield return AccountName; - } - - public string GetPrimaryAccount(StorageType storageType) { - throw new System.NotImplementedException(); + public IReadOnlyList 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 IStorage.CorpusAccounts() { + throw new NotImplementedException(); + } + + public IReadOnlyList CorpusAccounts() { + throw new System.NotImplementedException(); + } + + public string GetPrimaryAccount(StorageType storageType) { + throw new System.NotImplementedException(); + } + + public Task GetStorageAccountNameAndKeyByName(string accountName) { + throw new System.NotImplementedException(); + } } diff --git a/src/ApiService/IntegrationTests/Integration/AzuriteStorage.cs b/src/ApiService/IntegrationTests/Integration/AzuriteStorage.cs index 22cad534e..e995bd2dc 100644 --- a/src/ApiService/IntegrationTests/Integration/AzuriteStorage.cs +++ b/src/ApiService/IntegrationTests/Integration/AzuriteStorage.cs @@ -37,17 +37,17 @@ sealed class AzuriteStorage : IStorage { public Task<(string, string)> GetStorageAccountNameAndKey(string accountId) => Async.Task.FromResult((AccountName, AccountKey)); - public Task GetStorageAccountNameKeyByName(string accountName) { - return Async.Task.FromResult(AccountName)!; + public IReadOnlyList GetAccounts(StorageType storageType) { + return new[] { AccountName }; } - public IEnumerable CorpusAccounts() { + public IReadOnlyList CorpusAccounts() { throw new System.NotImplementedException(); } public string GetPrimaryAccount(StorageType storageType) => AccountName; - public IEnumerable GetAccounts(StorageType storageType) { - yield return AccountName; + public Task GetStorageAccountNameKeyByName(string accountName) { + throw new NotImplementedException(); } } diff --git a/src/ApiService/IntegrationTests/_FunctionTestBase.cs b/src/ApiService/IntegrationTests/_FunctionTestBase.cs index 184896a26..9bc8fc33a 100644 --- a/src/ApiService/IntegrationTests/_FunctionTestBase.cs +++ b/src/ApiService/IntegrationTests/_FunctionTestBase.cs @@ -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 {