Implement info Function in C# (#2061)

Adds implementation of the `info` function. 

Added support for blobs in the function integration test stuff.

Simplified usage of integration test classes by removing the account name parameter.
This commit is contained in:
George Pollard
2022-06-23 13:06:40 +12:00
committed by GitHub
parent 4cb7bba2c9
commit 1eeefce85c
19 changed files with 431 additions and 102 deletions

View File

@ -280,10 +280,11 @@ jobs:
run: |
cd src/ApiService/
dotnet format --verify-no-changes --no-restore
- name: Build Service
run: |
cd src/ApiService/
dotnet build -warnaserror --configuration Release
# note that Test comes before Build:
# Test will build things in Debug mode, Build will build them in Release
# Build comes second in case Test would do something (unspecified) that
# would somehow alter the binaries that we want to package.
- name: Test & Collect coverage info
run: |
cd src/ApiService/
@ -292,12 +293,30 @@ jobs:
dotnet tool run reportgenerator -reports:Tests/TestResults/*/coverage.cobertura.xml -targetdir:coverage -reporttypes:MarkdownSummary
kill %1
cat coverage/*.md > $GITHUB_STEP_SUMMARY
- name: Build Service
run: |
version=$(./src/ci/get-version.sh)
cd src/ApiService/
# Store GitHub RunID and SHA to be read by the 'info' function
echo ${GITHUB_RUN_ID} | tee ApiService/onefuzzlib/build.id
echo ${GITHUB_SHA} | tee ApiService/onefuzzlib/git.version
# stamp the build with version
# note that version might have a suffix of '-{sha}' from get-version.sh
if [[ "$version" =~ '-' ]]; then
# if it has a suffix, split it into two parts
dotnet build -warnaserror --configuration Release /p:VersionPrefix=${version%-*} /p:VersionSuffix=${version#*-}
else
dotnet build -warnaserror --configuration Release /p:VersionPrefix=${version}
fi
- name: copy artifacts
run: |
./src/ci/get-version.sh > src/deployment/VERSION
cd src/ApiService/ApiService/
echo ${GITHUB_RUN_ID} | tee onefuzzlib/build.id
echo ${GITHUB_SHA} | tee onefuzzlib/git.version
mv az-local.settings.json bin/Release/net6.0/local.settings.json
cd bin/Release/net6.0/
zip -r api-service-net.zip .

View File

@ -44,5 +44,11 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
<EmbeddedResource Include="onefuzzlib/build.id" Condition="Exists('onefuzzlib/build.id')">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="onefuzzlib/git.version" Condition="Exists('onefuzzlib/git.version')">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@ -0,0 +1,62 @@
using System.IO;
using System.Reflection;
using System.Threading;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
namespace Microsoft.OneFuzz.Service;
public class Info {
private readonly IOnefuzzContext _context;
private readonly IEndpointAuthorization _auth;
private readonly Lazy<Async.Task<InfoResponse>> _response;
public Info(IEndpointAuthorization auth, IOnefuzzContext context) {
_context = context;
_auth = auth;
// TODO: this isnt actually shared between calls at the moment,
// this needs to be placed into a class that can be registered into the
// DI container and shared amongst instances.
//
// However, we need to be careful about auth and caching between different
// credentials.
_response = new Lazy<Async.Task<InfoResponse>>(async () => {
var config = _context.ServiceConfiguration;
var resourceGroup = _context.Creds.GetBaseResourceGroup();
var subscription = _context.Creds.GetSubscription();
var region = await _context.Creds.GetBaseRegion();
var asm = Assembly.GetExecutingAssembly();
var gitVersion = ReadResource(asm, "ApiService.onefuzzlib.git.version");
var buildId = ReadResource(asm, "ApiService.onefuzzlib.build.id");
return new InfoResponse(
ResourceGroup: resourceGroup,
Subscription: subscription,
Region: region,
Versions: new Dictionary<string, InfoVersion> { { "onefuzz", new(gitVersion, buildId, config.OneFuzzVersion) } },
InstanceId: await _context.Containers.GetInstanceId(),
InsightsAppid: config.ApplicationInsightsAppId,
InsightsInstrumentationKey: config.ApplicationInsightsInstrumentationKey);
}, LazyThreadSafetyMode.PublicationOnly);
}
private static string ReadResource(Assembly asm, string resourceName) {
using var r = asm.GetManifestResourceStream(resourceName);
if (r is null) {
return "unknown";
}
using var sr = new StreamReader(r);
return sr.ReadToEnd();
}
private async Async.Task<HttpResponseData> GetResponse(HttpRequestData req)
=> await RequestHandling.Ok(req, await _response.Value);
// [Function("Info")]
public Async.Task<HttpResponseData> Run([HttpTrigger("GET")] HttpRequestData req)
=> _auth.CallIfUser(req, GetResponse);
}

View File

@ -19,6 +19,20 @@ public record BoolResult(
bool Result
) : BaseResponse();
public record InfoResponse(
string ResourceGroup,
string Region,
string Subscription,
IReadOnlyDictionary<string, InfoVersion> Versions,
Guid? InstanceId,
string? InsightsAppid,
string? InsightsInstrumentationKey
) : BaseResponse();
public record InfoVersion(
string Git,
string Build,
string Version);
public class BaseResponseConverter : JsonConverter<BaseResponse> {
public override BaseResponse? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {

View File

@ -106,6 +106,7 @@ public class Program {
.AddScoped<INodeMessageOperations, NodeMessageOperations>()
.AddScoped<IRequestHandling, RequestHandling>()
.AddScoped<IOnefuzzContext, OnefuzzContext>()
.AddScoped<IEndpointAuthorization, EndpointAuthorization>()
.AddSingleton<ICreds, Creds>()
.AddSingleton<IServiceConfig, ServiceConfiguration>()

View File

@ -39,10 +39,10 @@ public interface IServiceConfig {
public string OneFuzzVersion { get; }
// Prefix to add to the name of any tables created. This allows
// Prefix to add to the name of any tables & containers created. This allows
// multiple instances to run against the same storage account, which
// is useful for things like integration testing.
public string OneFuzzTablePrefix { get; }
public string OneFuzzStoragePrefix { get; }
}
public class ServiceConfiguration : IServiceConfig {
@ -87,5 +87,5 @@ public class ServiceConfiguration : IServiceConfig {
public string OneFuzzVersion { get => Environment.GetEnvironmentVariable("ONEFUZZ_VERSION") ?? "0.0.0"; }
public string OneFuzzNodeDisposalStrategy { get => Environment.GetEnvironmentVariable("ONEFUZZ_NODE_DISPOSAL_STRATEGY") ?? "scale_in"; }
public string OneFuzzTablePrefix => ""; // in production we never prefix the tables
public string OneFuzzStoragePrefix => ""; // in production we never prefix the tables
}

View File

@ -1,10 +1,10 @@
using Azure;
using System.Threading;
using Azure;
using Azure.ResourceManager;
using Azure.Storage;
using Azure.Storage.Blobs;
using Azure.Storage.Sas;
namespace Microsoft.OneFuzz.Service;
@ -32,12 +32,23 @@ public class Containers : IContainers {
private IStorage _storage;
private ICreds _creds;
private ArmClient _armClient;
private readonly IServiceConfig _config;
public Containers(ILogTracer log, IStorage storage, ICreds creds) {
public Containers(ILogTracer log, IStorage storage, ICreds creds, IServiceConfig config) {
_log = log;
_storage = storage;
_creds = creds;
_armClient = creds.ArmClient;
_config = config;
_getInstanceId = new Lazy<Async.Task<Guid>>(async () => {
var blob = await GetBlob(new Container("base-config"), "instance_id", StorageType.Config);
if (blob == null) {
throw new Exception("Blob Not Found");
}
return Guid.Parse(blob.ToString());
}, LazyThreadSafetyMode.PublicationOnly);
}
public async Async.Task<Uri?> GetFileUrl(Container container, string name, StorageType storageType) {
@ -72,9 +83,11 @@ public class Containers : IContainers {
// # Secondary accounts, if they exist, are preferred for containers and have
// # increased IOP rates, this should be a slight optimization
var containerName = _config.OneFuzzStoragePrefix + container.ContainerName;
var containers = _storage.GetAccounts(storageType)
.Reverse()
.Select(async account => (await GetBlobService(account))?.GetBlobContainerClient(container.ContainerName));
.Select(async account => (await GetBlobService(account))?.GetBlobContainerClient(containerName));
foreach (var c in containers) {
var client = await c;
@ -86,7 +99,7 @@ public class Containers : IContainers {
}
private async Async.Task<BlobServiceClient?> GetBlobService(string accountId) {
_log.Info($"getting blob container (account_id: {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");
@ -133,14 +146,8 @@ public class Containers : IContainers {
await client.UploadBlobAsync(name, new BinaryData(data));
}
//TODO: get this ones on startup and cache (and make this method un-accessible to everyone else)
public async Async.Task<Guid> GetInstanceId() {
var blob = await GetBlob(new Container("base-config"), "instance_id", StorageType.Config);
if (blob == null) {
throw new System.Exception("Blob Not Found");
}
return System.Guid.Parse(blob.ToString());
}
public Async.Task<Guid> GetInstanceId() => _getInstanceId.Value;
private readonly Lazy<Async.Task<Guid>> _getInstanceId;
public Uri? GetContainerSasUrlService(
BlobContainerClient client,

View File

@ -3,7 +3,25 @@ using Microsoft.Azure.Functions.Worker.Http;
namespace Microsoft.OneFuzz.Service;
public class EndpointAuthorization {
public interface IEndpointAuthorization {
Async.Task<HttpResponseData> CallIfAgent(
HttpRequestData req,
Func<HttpRequestData, Async.Task<HttpResponseData>> method)
=> CallIf(req, method, allowAgent: true);
Async.Task<HttpResponseData> CallIfUser(
HttpRequestData req,
Func<HttpRequestData, Async.Task<HttpResponseData>> method)
=> CallIf(req, method, allowUser: true);
Async.Task<HttpResponseData> CallIf(
HttpRequestData req,
Func<HttpRequestData, Async.Task<HttpResponseData>> method,
bool allowUser = false,
bool allowAgent = false);
}
public class EndpointAuthorization : IEndpointAuthorization {
private readonly IOnefuzzContext _context;
private readonly ILogTracer _log;
@ -11,9 +29,6 @@ public class EndpointAuthorization {
_context = context;
_log = log;
}
public async Async.Task<HttpResponseData> CallIfAgent(HttpRequestData req, Func<HttpRequestData, Async.Task<HttpResponseData>> method) {
return await CallIf(req, method, allowAgent: true);
}
public async Async.Task<HttpResponseData> CallIf(HttpRequestData req, Func<HttpRequestData, Async.Task<HttpResponseData>> method, bool allowUser = false, bool allowAgent = false) {
var tokenResult = await _context.UserCredentials.ParseJwtToken(req);

View File

@ -93,7 +93,7 @@ namespace ApiService.OneFuzzLib.Orm {
public async Task<TableClient> GetTableClient(string table, string? accountId = null) {
// TODO: do this less often, instead of once per request:
var tableName = _context.ServiceConfiguration.OneFuzzTablePrefix + table;
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);

View File

@ -10,10 +10,12 @@ namespace Tests.Fakes;
// TestContext provides a minimal IOnefuzzContext implementation to allow running
// of functions as unit or integration tests.
public sealed class TestContext : IOnefuzzContext {
public TestContext(ILogTracer logTracer, IStorage storage, string tablePrefix, string accountId) {
ServiceConfiguration = new TestServiceConfiguration(tablePrefix, accountId);
public TestContext(ILogTracer logTracer, IStorage storage, ICreds creds, string storagePrefix) {
ServiceConfiguration = new TestServiceConfiguration(storagePrefix);
Storage = storage;
Creds = creds;
Containers = new Containers(logTracer, Storage, Creds, ServiceConfiguration);
RequestHandling = new RequestHandling(logTracer);
TaskOperations = new TaskOperations(logTracer, this);
@ -44,6 +46,8 @@ public sealed class TestContext : IOnefuzzContext {
public IServiceConfig ServiceConfiguration { get; }
public IStorage Storage { get; }
public ICreds Creds { get; }
public IContainers Containers { get; }
public IRequestHandling RequestHandling { get; }
@ -60,10 +64,6 @@ public sealed class TestContext : IOnefuzzContext {
public IConfigOperations ConfigOperations => throw new System.NotImplementedException();
public IContainers Containers => throw new System.NotImplementedException();
public ICreds Creds => throw new System.NotImplementedException();
public IDiskOperations DiskOperations => throw new System.NotImplementedException();
public IExtensions Extensions => throw new System.NotImplementedException();

View File

@ -0,0 +1,56 @@
using System;
using System.Threading.Tasks;
using Azure.Core;
using Azure.Identity;
using Azure.ResourceManager;
using Azure.ResourceManager.Resources;
using Microsoft.OneFuzz.Service;
using Task = System.Threading.Tasks.Task;
namespace Tests.Fakes;
class TestCreds : ICreds {
private readonly Guid _subscriptionId;
private readonly string _resourceGroup;
private readonly string _region;
public TestCreds(Guid subscriptionId, string resourceGroup, string region) {
_subscriptionId = subscriptionId;
_resourceGroup = resourceGroup;
_region = region;
}
public ArmClient ArmClient => null!;
// we have to return something in some test cases, even if it isnt used
public Task<string> GetBaseRegion() => Task.FromResult(_region);
public string GetBaseResourceGroup() => _resourceGroup;
public string GetSubscription() => _subscriptionId.ToString();
public DefaultAzureCredential GetIdentity() {
throw new NotImplementedException();
}
public string GetInstanceName() {
throw new NotImplementedException();
}
public Uri GetInstanceUrl() {
throw new NotImplementedException();
}
public ResourceGroupResource GetResourceGroupResource() {
throw new NotImplementedException();
}
public ResourceIdentifier GetResourceGroupResourceIdentifier() {
throw new NotImplementedException();
}
public Guid GetScalesetPrincipalId() {
throw new NotImplementedException();
}
}

View File

@ -0,0 +1,44 @@

using System;
using System.Net;
using System.Threading.Tasks;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.OneFuzz.Service;
enum RequestType {
NoAuthorization,
User,
Agent,
}
sealed class TestEndpointAuthorization : IEndpointAuthorization {
private readonly RequestType _type;
private readonly IOnefuzzContext _context;
public TestEndpointAuthorization(RequestType type, IOnefuzzContext context) {
_type = type;
_context = context;
}
public Task<HttpResponseData> CallIf(
HttpRequestData req,
Func<HttpRequestData, Task<HttpResponseData>> method,
bool allowUser = false,
bool allowAgent = false) {
if ((_type == RequestType.User && allowUser) ||
(_type == RequestType.Agent && allowAgent)) {
return method(req);
}
return _context.RequestHandling.NotOk(
req,
new Error(
ErrorCode.UNAUTHORIZED,
new string[] { "Unrecognized agent" }
),
"token verification",
HttpStatusCode.Unauthorized
);
}
}

View File

@ -44,6 +44,9 @@ sealed class TestHttpRequestData : HttpRequestData {
public static TestHttpRequestData FromJson<T>(string method, T obj)
=> new(method, Serializer.Serialize(obj));
public static TestHttpRequestData Empty(string method)
=> new(method, new BinaryData(Array.Empty<byte>()));
public TestHttpRequestData(string method, BinaryData body)
: base(NewFunctionContext()) {
Method = method;

View File

@ -1,29 +1,30 @@
using Microsoft.ApplicationInsights.DataContracts;
using System;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.OneFuzz.Service;
namespace Tests.Fakes;
sealed class TestServiceConfiguration : IServiceConfig {
public TestServiceConfiguration(string tablePrefix, string accountId) {
OneFuzzTablePrefix = tablePrefix;
OneFuzzFuncStorage = accountId;
public sealed class TestServiceConfiguration : IServiceConfig {
public TestServiceConfiguration(string tablePrefix) {
OneFuzzStoragePrefix = tablePrefix;
}
public string OneFuzzTablePrefix { get; }
public string OneFuzzStoragePrefix { get; }
public string? OneFuzzFuncStorage { get; }
public string? OneFuzzFuncStorage { get; } = "UNUSED_ACCOUNT_ID"; // test implementations do not use this
public string OneFuzzVersion => "9999.0.0"; // very big version to pass any >= checks
public string? ApplicationInsightsAppId { get; set; } = "TestAppInsightsAppId";
public string? ApplicationInsightsInstrumentationKey { get; set; } = "TestAppInsightsInstrumentationKey";
// -- Remainder not implemented --
public LogDestination[] LogDestinations { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
public SeverityLevel LogSeverityLevel => throw new System.NotImplementedException();
public string? ApplicationInsightsAppId => throw new System.NotImplementedException();
public string? ApplicationInsightsInstrumentationKey => throw new System.NotImplementedException();
public string? AzureSignalRConnectionString => throw new System.NotImplementedException();
@ -39,8 +40,6 @@ sealed class TestServiceConfiguration : IServiceConfig {
public string? MultiTenantDomain => throw new System.NotImplementedException();
public string? OneFuzzDataStorage => throw new System.NotImplementedException();
public string? OneFuzzInstance => throw new System.NotImplementedException();
public string? OneFuzzInstanceName => throw new System.NotImplementedException();
@ -53,7 +52,9 @@ sealed class TestServiceConfiguration : IServiceConfig {
public string OneFuzzNodeDisposalStrategy => throw new System.NotImplementedException();
public string? OneFuzzResourceGroup => throw new System.NotImplementedException();
public string? OneFuzzTelemetry => throw new System.NotImplementedException();
public string? OneFuzzDataStorage => throw new NotImplementedException();
public string? OneFuzzResourceGroup => throw new NotImplementedException();
}

View File

@ -13,17 +13,17 @@ namespace Tests.Functions;
[Trait("Category", "Integration")]
public class AzureStorageAgentEventsTest : AgentEventsTestsBase {
public AzureStorageAgentEventsTest(ITestOutputHelper output)
: base(output, Integration.AzureStorage.FromEnvironment(), "UNUSED") { }
: base(output, Integration.AzureStorage.FromEnvironment()) { }
}
public class AzuriteAgentEventsTest : AgentEventsTestsBase {
public AzuriteAgentEventsTest(ITestOutputHelper output)
: base(output, new Integration.AzuriteStorage(), "devstoreaccount1") { }
: base(output, new Integration.AzuriteStorage()) { }
}
public abstract class AgentEventsTestsBase : FunctionTestBase {
public AgentEventsTestsBase(ITestOutputHelper output, IStorage storage, string accountId)
: base(output, storage, accountId) { }
public AgentEventsTestsBase(ITestOutputHelper output, IStorage storage)
: base(output, storage) { }
// shared helper variables (per-test)
readonly Guid jobId = Guid.NewGuid();

View File

@ -0,0 +1,65 @@

using System;
using System.Net;
using Microsoft.OneFuzz.Service;
using Tests.Fakes;
using Xunit;
using Xunit.Abstractions;
using Async = System.Threading.Tasks;
namespace Tests.Functions;
[Trait("Category", "Integration")]
public class AzureStorageInfoTest : InfoTestBase {
public AzureStorageInfoTest(ITestOutputHelper output)
: base(output, Integration.AzureStorage.FromEnvironment()) { }
}
public class AzuriteInfoTest : InfoTestBase {
public AzuriteInfoTest(ITestOutputHelper output)
: base(output, new Integration.AzuriteStorage()) { }
}
public abstract class InfoTestBase : FunctionTestBase {
public InfoTestBase(ITestOutputHelper output, IStorage storage)
: base(output, storage) { }
[Fact]
public async Async.Task TestInfo_WithoutAuthorization_IsRejected() {
var auth = new TestEndpointAuthorization(RequestType.NoAuthorization, Context);
var func = new Info(auth, Context);
var result = await func.Run(TestHttpRequestData.Empty("GET"));
Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode);
}
[Fact]
public async Async.Task TestInfo_WithAgentCredentials_IsRejected() {
var auth = new TestEndpointAuthorization(RequestType.Agent, Context);
var func = new Info(auth, Context);
var result = await func.Run(TestHttpRequestData.Empty("GET"));
Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode);
}
[Fact]
public async Async.Task TestInfo_WithUserCredentials_Succeeds() {
// store the instance ID in the expected location:
// for production this is done by the deploy script
var instanceId = Guid.NewGuid().ToString();
var containerClient = GetContainerClient("base-config");
await containerClient.CreateAsync();
await containerClient.GetBlobClient("instance_id").UploadAsync(new BinaryData(instanceId));
var auth = new TestEndpointAuthorization(RequestType.User, Context);
var func = new Info(auth, Context);
var result = await func.Run(TestHttpRequestData.Empty("GET"));
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
// the instance ID should be somewhere in the result,
// indicating it was read from the blob
Assert.Contains(instanceId, BodyAsString(result));
}
}

View File

@ -1,7 +1,10 @@
using System;
using System.IO;
using System.Linq;
using ApiService.OneFuzzLib.Orm;
using Azure.Data.Tables;
using Azure.Storage;
using Azure.Storage.Blobs;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.OneFuzz.Service;
using Tests.Fakes;
@ -22,19 +25,33 @@ namespace Tests.Functions;
public abstract class FunctionTestBase : IDisposable {
private readonly IStorage _storage;
// each test will use a different table prefix so they don't interfere
// each test will use a different prefix for storage (tables, blobs) so they don't interfere
// with each other - generate a prefix like t12345678 (table names must start with letter)
private readonly string _tablePrefix = "t" + Guid.NewGuid().ToString()[..8];
private readonly string _storagePrefix = "t" + Guid.NewGuid().ToString()[..8];
private readonly Guid _subscriptionId = Guid.NewGuid();
private readonly string _resourceGroup = "FakeResourceGroup";
private readonly string _region = "fakeregion";
protected ILogTracer Logger { get; }
protected TestContext Context { get; }
public FunctionTestBase(ITestOutputHelper output, IStorage storage, string accountId) {
private readonly BlobServiceClient _blobClient;
protected BlobContainerClient GetContainerClient(string container)
=> _blobClient.GetBlobContainerClient(_storagePrefix + container);
public FunctionTestBase(ITestOutputHelper output, IStorage storage) {
Logger = new TestLogTracer(output);
_storage = storage;
Context = new TestContext(Logger, _storage, _tablePrefix, accountId);
var creds = new TestCreds(_subscriptionId, _resourceGroup, _region);
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));
}
protected static string BodyAsString(HttpResponseData data) {
@ -44,18 +61,36 @@ public abstract class FunctionTestBase : IDisposable {
}
public void Dispose() {
// TODO, a bit ugly, tidy this up:
// delete any tables we created during the run
if (_storage is Integration.AzureStorage storage) {
var accountName = storage.AccountName;
var accountKey = storage.AccountKey;
var (accountName, accountKey) = _storage.GetStorageAccountNameAndKey("").Result; // sync for test impls
if (accountName is not null && accountKey is not null) {
// we are running against live storage
var tableClient = new TableServiceClient(
_storage.GetTableEndpoint(accountName),
// clean up any tables & blobs that this test created
CleanupTables(_storage.GetTableEndpoint(accountName),
new TableSharedKeyCredential(accountName, accountKey));
var tablesToDelete = tableClient.Query(filter: Query.StartsWith("TableName", _tablePrefix));
CleanupBlobs(_storage.GetBlobEndpoint(accountName),
new StorageSharedKeyCredential(accountName, accountKey));
}
}
private void CleanupBlobs(Uri endpoint, StorageSharedKeyCredential creds) {
var blobClient = new BlobServiceClient(endpoint, creds);
var containersToDelete = blobClient.GetBlobContainers(prefix: _storagePrefix);
foreach (var container in containersToDelete.Where(c => c.IsDeleted != true)) {
try {
blobClient.DeleteBlobContainer(container.Name);
Logger.Info($"cleaned up container {container.Name}");
} catch (Exception ex) {
// swallow any exceptions: this is a best-effort attempt to cleanup
Logger.Exception(ex, "error deleting container at end of test");
}
}
}
private void CleanupTables(Uri endpoint, TableSharedKeyCredential creds) {
var tableClient = new TableServiceClient(endpoint, creds);
var tablesToDelete = tableClient.Query(filter: Query.StartsWith("TableName", _storagePrefix));
foreach (var table in tablesToDelete) {
try {
tableClient.DeleteTable(table.Name);
@ -66,6 +101,4 @@ public abstract class FunctionTestBase : IDisposable {
}
}
}
}
}
}

View File

@ -33,23 +33,13 @@ sealed class AzureStorage : IStorage {
AccountKey = accountKey;
}
public IEnumerable<string> CorpusAccounts() {
throw new System.NotImplementedException();
}
public IEnumerable<string> GetAccounts(StorageType storageType) {
throw new System.NotImplementedException();
}
public string GetPrimaryAccount(StorageType storageType) {
throw new System.NotImplementedException();
}
public Task<(string?, string?)> GetStorageAccountNameAndKey(string accountId)
=> Async.Task.FromResult((AccountName, AccountKey));
public Task<string?> GetStorageAccountNameAndKeyByName(string accountName) {
throw new System.NotImplementedException();
public IEnumerable<string> GetAccounts(StorageType storageType) {
if (AccountName != null) {
yield return AccountName;
}
}
public Uri GetTableEndpoint(string accountId)
@ -60,4 +50,16 @@ sealed class AzureStorage : IStorage {
public Uri GetBlobEndpoint(string accountId)
=> new($"https://{AccountName}.blob.core.windows.net/");
public IEnumerable<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

@ -8,21 +8,26 @@ using Async = System.Threading.Tasks;
namespace Tests.Integration;
sealed class AzuriteStorage : IStorage {
public Uri GetBlobEndpoint(string accountId)
=> new($"http://127.0.0.1:10000/{accountId}");
public Uri GetBlobEndpoint(string _accountId)
=> new($"http://127.0.0.1:10000/devstoreaccount1");
public Uri GetQueueEndpoint(string accountId)
=> new($"http://127.0.0.1:10001/{accountId}");
public Uri GetQueueEndpoint(string _accountId)
=> new($"http://127.0.0.1:10001/devstoreaccount1");
public Uri GetTableEndpoint(string accountId)
=> new($"http://127.0.0.1:10002/{accountId}");
public Uri GetTableEndpoint(string _accountId)
=> new($"http://127.0.0.1:10002/devstoreaccount1");
// This is the fixed account key used by Azurite (derived from devstorage emulator);
// This is the fixed name & account key used by Azurite (derived from devstorage emulator);
// https://docs.microsoft.com/en-us/azure/storage/common/storage-configure-connection-string#configure-a-connection-string-for-azurite
const string AccountName = "devstoreaccount1";
const string AccountKey = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==";
public Task<(string?, string?)> GetStorageAccountNameAndKey(string accountId)
=> Async.Task.FromResult<(string?, string?)>((accountId, AccountKey));
public Task<(string?, string?)> GetStorageAccountNameAndKey(string _accountId)
=> Async.Task.FromResult<(string?, string?)>((AccountName, AccountKey));
public IEnumerable<string> GetAccounts(StorageType storageType) {
yield return AccountName;
}
public Task<string?> GetStorageAccountNameAndKeyByName(string accountName) {
throw new System.NotImplementedException();
@ -32,10 +37,6 @@ sealed class AzuriteStorage : IStorage {
throw new System.NotImplementedException();
}
public IEnumerable<string> GetAccounts(StorageType storageType) {
throw new System.NotImplementedException();
}
public string GetPrimaryAccount(StorageType storageType) {
throw new System.NotImplementedException();
}