mirror of
https://github.com/microsoft/onefuzz.git
synced 2025-06-14 11:08:06 +00:00
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:
117
src/ApiService/ApiService/Containers.cs
Normal file
117
src/ApiService/ApiService/Containers.cs
Normal 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));
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
|
175
src/ApiService/IntegrationTests/ContainersTests.cs
Normal file
175
src/ApiService/IntegrationTests/ContainersTests.cs
Normal 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
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
// 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 {
|
||||
|
Reference in New Issue
Block a user