instance config (#1791)

* instance config

* address PR comments

* make logs scoped

* make logs scoped

Co-authored-by: stas <statis@microsoft.com>
This commit is contained in:
Stas
2022-04-14 14:20:28 -07:00
committed by GitHub
parent b03d420804
commit 22faa1b5db
23 changed files with 539 additions and 136 deletions

View File

@ -5,6 +5,7 @@
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
<WarningLevel>5</WarningLevel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Storage" Version="5.0.0" />

View File

@ -22,6 +22,9 @@ public static class EnvironmentVariables
//TODO: Add environment variable to control where to write logs to
public static LogDestination[] LogDestinations { get; set; }
//TODO: Get this from Environment variable
public static ApplicationInsights.DataContracts.SeverityLevel LogSeverityLevel() { return ApplicationInsights.DataContracts.SeverityLevel.Verbose; }
public static class AppInsights
{
public static string? AppId { get => Environment.GetEnvironmentVariable("APPINSIGHTS_APPID"); }

View File

@ -1,40 +0,0 @@
using System;
using System.Threading.Tasks;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
namespace Microsoft.OneFuzz.Service;
public record FunctionInfo(string Name, string ResourceGroup, string? SlotName);
public class Info
{
private readonly ILogTracerFactory _loggerFactory;
public Info(ILogTracerFactory loggerFactory)
{
_loggerFactory = loggerFactory;
}
[Function("Info")]
public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequestData req)
{
var log = _loggerFactory.MakeLogTracer(Guid.NewGuid());
log.Info("Creating function info response");
var response = req.CreateResponse();
FunctionInfo info = new(
$"{EnvironmentVariables.OneFuzz.InstanceName}",
$"{EnvironmentVariables.OneFuzz.ResourceGroup}",
Environment.GetEnvironmentVariable("WEBSITE_SLOT_NAME"));
log.Info("Returning function info");
await response.WriteAsJsonAsync(info);
log.Info("Returned function info");
return response;
}
}

View File

@ -132,22 +132,34 @@ public interface ILogTracer
void ForceFlush();
void Info(string message);
void Warning(string message);
void Verbose(string message);
ILogTracer AddTags((string, string)[]? tags);
ILogTracer WithTag(string k, string v);
ILogTracer WithTags((string, string)[]? tags);
}
public class LogTracer : ILogTracer
internal interface ILogTracerInternal : ILogTracer
{
void ReplaceCorrelationId(Guid newCorrelationId);
void AddTags((string, string)[] tags);
}
public class LogTracer : ILogTracerInternal
{
private string? GetCaller()
{
return new StackTrace()?.GetFrame(2)?.GetMethod()?.DeclaringType?.FullName;
}
private Guid _correlationId;
private List<ILog> _loggers;
private Dictionary<string, string> _tags;
private SeverityLevel _logSeverityLevel;
public Guid CorrelationId { get; }
public IReadOnlyDictionary<string, string> Tags { get; }
public Guid CorrelationId => _correlationId;
public IReadOnlyDictionary<string, string> Tags => _tags;
private static List<KeyValuePair<string, string>> ConvertTags((string, string)[]? tags)
{
@ -166,17 +178,43 @@ public class LogTracer : ILogTracer
}
}
public LogTracer(Guid correlationId, (string, string)[]? tags, List<ILog> loggers) : this(correlationId, new Dictionary<string, string>(ConvertTags(tags)), loggers) { }
public LogTracer(Guid correlationId, (string, string)[]? tags, List<ILog> loggers, SeverityLevel logSeverityLevel) :
this(correlationId, new Dictionary<string, string>(ConvertTags(tags)), loggers, logSeverityLevel)
{ }
public LogTracer(Guid correlationId, IReadOnlyDictionary<string, string> tags, List<ILog> loggers)
public LogTracer(Guid correlationId, IReadOnlyDictionary<string, string> tags, List<ILog> loggers, SeverityLevel logSeverityLevel)
{
CorrelationId = correlationId;
Tags = tags;
_correlationId = correlationId;
_tags = new(tags);
_loggers = loggers;
_logSeverityLevel = logSeverityLevel;
}
public ILogTracer AddTags((string, string)[]? tags)
//Single threaded only
public void ReplaceCorrelationId(Guid newCorrelationId)
{
_correlationId = newCorrelationId;
}
//single threaded only
public void AddTags((string, string)[] tags)
{
if (tags is not null)
{
foreach (var (k, v) in tags)
{
_tags[k] = v;
}
}
}
public ILogTracer WithTag(string k, string v)
{
return WithTags(new[] { (k, v) });
}
public ILogTracer WithTags((string, string)[]? tags)
{
var newTags = new Dictionary<string, string>(Tags);
if (tags is not null)
@ -186,42 +224,66 @@ public class LogTracer : ILogTracer
newTags[k] = v;
}
}
return new LogTracer(CorrelationId, newTags, _loggers);
return new LogTracer(CorrelationId, newTags, _loggers, _logSeverityLevel);
}
public void Verbose(string message)
{
if (_logSeverityLevel >= SeverityLevel.Verbose)
{
var caller = GetCaller();
foreach (var logger in _loggers)
{
logger.Log(CorrelationId, message, SeverityLevel.Verbose, Tags, caller);
}
}
}
public void Info(string message)
{
var caller = GetCaller();
foreach (var logger in _loggers)
if (_logSeverityLevel >= SeverityLevel.Information)
{
logger.Log(CorrelationId, message, SeverityLevel.Information, Tags, caller);
var caller = GetCaller();
foreach (var logger in _loggers)
{
logger.Log(CorrelationId, message, SeverityLevel.Information, Tags, caller);
}
}
}
public void Warning(string message)
{
var caller = GetCaller();
foreach (var logger in _loggers)
if (_logSeverityLevel >= SeverityLevel.Warning)
{
logger.Log(CorrelationId, message, SeverityLevel.Warning, Tags, caller);
var caller = GetCaller();
foreach (var logger in _loggers)
{
logger.Log(CorrelationId, message, SeverityLevel.Warning, Tags, caller);
}
}
}
public void Error(string message)
{
var caller = GetCaller();
foreach (var logger in _loggers)
if (_logSeverityLevel >= SeverityLevel.Error)
{
logger.Log(CorrelationId, message, SeverityLevel.Error, Tags, caller);
var caller = GetCaller();
foreach (var logger in _loggers)
{
logger.Log(CorrelationId, message, SeverityLevel.Error, Tags, caller);
}
}
}
public void Critical(string message)
{
var caller = GetCaller();
foreach (var logger in _loggers)
if (_logSeverityLevel >= SeverityLevel.Critical)
{
logger.Log(CorrelationId, message, SeverityLevel.Critical, Tags, caller);
var caller = GetCaller();
foreach (var logger in _loggers)
{
logger.Log(CorrelationId, message, SeverityLevel.Critical, Tags, caller);
}
}
}
@ -254,7 +316,7 @@ public class LogTracer : ILogTracer
public interface ILogTracerFactory
{
LogTracer MakeLogTracer(Guid correlationId, (string, string)[]? tags = null);
LogTracer CreateLogTracer(Guid correlationId, (string, string)[]? tags = null, SeverityLevel severityLevel = SeverityLevel.Verbose);
}
public class LogTracerFactory : ILogTracerFactory
@ -266,9 +328,9 @@ public class LogTracerFactory : ILogTracerFactory
_loggers = loggers;
}
public LogTracer MakeLogTracer(Guid correlationId, (string, string)[]? tags = null)
public LogTracer CreateLogTracer(Guid correlationId, (string, string)[]? tags = null, SeverityLevel severityLevel = SeverityLevel.Verbose)
{
return new(correlationId, tags, _loggers);
return new(correlationId, tags, _loggers, severityLevel);
}
}

View File

@ -1,4 +1,5 @@
public enum ErrorCode
namespace Microsoft.OneFuzz.Service;
public enum ErrorCode
{
INVALID_REQUEST = 450,
INVALID_PERMISSION = 451,

View File

@ -43,6 +43,7 @@ namespace Microsoft.OneFuzz.Service
this switch
{
EventNodeHeartbeat _ => EventType.NodeHeartbeat,
EventInstanceConfigUpdated _ => EventType.InstanceConfigUpdated,
_ => throw new NotImplementedException(),
};
@ -243,7 +244,7 @@ namespace Microsoft.OneFuzz.Service
// ) : BaseEvent();
// record EventInstanceConfigUpdated(
// InstanceConfig Config
// ) : BaseEvent();
record EventInstanceConfigUpdated(
InstanceConfig Config
) : BaseEvent();
}

View File

@ -1,13 +1,17 @@
using Microsoft.OneFuzz.Service.OneFuzzLib.Orm;
using System;
using System.Collections.Generic;
using PoolName = System.String;
using Region = System.String;
using System.Text.Json.Serialization;
using Container = System.String;
using Region = System.String;
using PoolName = System.String;
using Endpoint = System.String;
using GroupId = System.Guid;
using PrincipalId = System.Guid;
namespace Microsoft.OneFuzz.Service;
/// Convention for database entities:
/// All entities are represented by immutable records
/// All database entities need to derive from EntityBase
@ -17,6 +21,7 @@ namespace Microsoft.OneFuzz.Service;
/// the "partion key" and "row key" are identified by the [PartitionKey] and [RowKey] attributes
/// Guids are mapped to string in the db
public record Authentication
(
string Password,
@ -253,5 +258,118 @@ public record Task(
{
List<TaskEventSummary> Events { get; set; } = new List<TaskEventSummary>();
List<NodeAssignment> Nodes { get; set; } = new List<NodeAssignment>();
}
public record AzureSecurityExtensionConfig();
public record GenevaExtensionConfig();
}
public record KeyvaultExtensionConfig(
string KeyVaultName,
string CertName,
string CertPath,
string ExtensionStore
);
public record AzureMonitorExtensionConfig(
string ConfigVersion,
string Moniker,
string Namespace,
[property: JsonPropertyName("monitoringGSEnvironment")] string MonitoringGSEnvironment,
[property: JsonPropertyName("monitoringGCSAccount")] string MonitoringGCSAccount,
[property: JsonPropertyName("monitoringGCSAuthId")] string MonitoringGCSAuthId,
[property: JsonPropertyName("monitoringGCSAuthIdType")] string MonitoringGCSAuthIdType
);
public record AzureVmExtensionConfig(
KeyvaultExtensionConfig? Keyvault,
AzureMonitorExtensionConfig AzureMonitor
);
public record NetworkConfig(
string AddressSpace,
string Subnet
)
{
public NetworkConfig() : this("10.0.0.0/8", "10.0.0.0/16") { }
}
public record NetworkSecurityGroupConfig(
string[] AllowedServiceTags,
string[] AllowedIps
)
{
public NetworkSecurityGroupConfig() : this(Array.Empty<string>(), Array.Empty<string>()) { }
}
public record ApiAccessRule(
string[] Methods,
Guid[] AllowedGroups
);
public record InstanceConfig
(
[PartitionKey, RowKey] string InstanceName,
//# initial set of admins can only be set during deployment.
//# if admins are set, only admins can update instance configs.
Guid[]? Admins,
//# if set, only admins can manage pools or scalesets
bool AllowPoolManagement,
string[] AllowedAadTenants,
NetworkConfig NetworkConfig,
NetworkSecurityGroupConfig ProxyNsgConfig,
AzureVmExtensionConfig? Extensions,
string ProxyVmSku,
IDictionary<Endpoint, ApiAccessRule>? ApiAccessRules,
IDictionary<PrincipalId, GroupId[]>? GroupMembership,
IDictionary<string, string>? VmTags,
IDictionary<string, string>? VmssTags
) : EntityBase()
{
public InstanceConfig(string instanceName) : this(
instanceName,
null,
true,
Array.Empty<string>(),
new NetworkConfig(),
new NetworkSecurityGroupConfig(),
null,
"Standard_B2s",
null,
null,
null,
null)
{ }
public List<Guid>? CheckAdmins(List<Guid>? value)
{
if (value is not null && value.Count == 0)
{
throw new ArgumentException("admins must be null or contain at least one UUID");
}
else
{
return value;
}
}
//# At the moment, this only checks allowed_aad_tenants, however adding
//# support for 3rd party JWT validation is anticipated in a future release.
public ResultOk<List<string>> CheckInstanceConfig()
{
List<string> errors = new();
if (AllowedAadTenants.Length == 0)
{
errors.Add("allowed_aad_tenants must not be empty");
}
if (errors.Count == 0)
{
return ResultOk<List<string>>.Ok();
}
else
{
return ResultOk<List<string>>.Error(errors);
}
}
}

View File

@ -1,6 +1,24 @@
namespace Microsoft.OneFuzz.Service
{
public struct ResultOk<T_Error>
{
public static ResultOk<T_Error> Ok() => new();
public static ResultOk<T_Error> Error(T_Error err) => new(err);
readonly T_Error? error;
readonly bool isOk;
public ResultOk() => (error, isOk) = (default, true);
public ResultOk(T_Error error) => (this.error, isOk) = (error, false);
public bool IsOk => isOk;
public T_Error? ErrorV => error;
}
public struct Result<T_Ok, T_Error>
{
public static Result<T_Ok, T_Error> Ok(T_Ok ok) => new(ok);
@ -14,7 +32,10 @@
public Result(T_Error error) => (this.error, ok, isOk) = (error, default, false);
public bool IsOk => IsOk;
public bool IsOk => isOk;
public T_Error? ErrorV => error;
public T_Ok? OkV => ok;
}

View File

@ -6,13 +6,36 @@ using System.Collections.Generic;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using ApiService.OneFuzzLib;
using Microsoft.Azure.Functions.Worker.Middleware;
using Microsoft.Azure.Functions.Worker;
namespace Microsoft.OneFuzz.Service;
public class Program
{
public class LoggingMiddleware : IFunctionsWorkerMiddleware
{
public async Async.Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
{
//TODO
//if correlation ID is available in HTTP request
//if correlation ID is available in Queue message
//log.ReplaceCorrelationId
var log = (ILogTracerInternal?)context.InstanceServices.GetService<ILogTracer>();
if (log is not null)
{
log.AddTags(new[] {
("InvocationId", context.InvocationId.ToString())
});
}
await next(context);
}
}
public static List<ILog> GetLoggers()
{
List<ILog> loggers = new List<ILog>();
@ -23,7 +46,7 @@ public class Program
{
LogDestination.AppInsights => new AppInsights(),
LogDestination.Console => new Console(),
_ => throw new Exception(string.Format("Unhandled Log Destination type: {0}", dest)),
_ => throw new Exception($"Unhandled Log Destination type: {dest}"),
}
);
}
@ -34,19 +57,25 @@ public class Program
public static void Main()
{
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureFunctionsWorkerDefaults(
builder =>
{
builder.UseMiddleware<LoggingMiddleware>();
}
)
.ConfigureServices((context, services) =>
services
.AddSingleton<ILogTracerFactory>(_ => new LogTracerFactory(GetLoggers()))
.AddScoped<ILogTracer>(_ => new LogTracerFactory(GetLoggers()).CreateLogTracer(Guid.NewGuid(), severityLevel: EnvironmentVariables.LogSeverityLevel()))
.AddSingleton<INodeOperations, NodeOperations>()
.AddSingleton<IEvents, Events>()
.AddSingleton<IWebhookOperations, WebhookOperations>()
.AddSingleton<IWebhookMessageLogOperations, WebhookMessageLogOperations>()
.AddSingleton<ITaskOperations, TaskOperations>()
.AddSingleton<IQueue, Queue>()
.AddSingleton<ICreds>(_ => new Creds())
.AddSingleton<ICreds, Creds>()
.AddSingleton<IStorage, Storage>()
.AddSingleton<IProxyOperations, ProxyOperations>()
.AddSingleton<IConfigOperations, ConfigOperations>()
)
.Build();

View File

@ -13,13 +13,13 @@ public class QueueFileChanges
// https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-storage-queue-trigger?tabs=csharp#poison-messages
const int MAX_DEQUEUE_COUNT = 5;
private readonly ILogTracerFactory _loggerFactory;
private readonly ILogTracer _log;
private readonly IStorage _storage;
public QueueFileChanges(ILogTracerFactory loggerFactory, IStorage storage)
public QueueFileChanges(ILogTracer log, IStorage storage)
{
_loggerFactory = loggerFactory;
_log = log;
_storage = storage;
}
@ -28,7 +28,6 @@ public class QueueFileChanges
[QueueTrigger("file-changes-refactored", Connection = "AzureWebJobsStorage")] string msg,
int dequeueCount)
{
var log = _loggerFactory.MakeLogTracer(Guid.NewGuid());
var fileChangeEvent = JsonSerializer.Deserialize<Dictionary<string, string>>(msg, EntityConverter.GetJsonSerializerOptions());
var lastTry = dequeueCount == MAX_DEQUEUE_COUNT;
@ -44,12 +43,12 @@ public class QueueFileChanges
const string topic = "topic";
if (!fileChangeEvent.ContainsKey(topic)
|| !_storage.CorpusAccounts(log).Contains(fileChangeEvent[topic]))
|| !_storage.CorpusAccounts().Contains(fileChangeEvent[topic]))
{
return Async.Task.CompletedTask;
}
file_added(log, fileChangeEvent, lastTry);
file_added(_log, fileChangeEvent, lastTry);
return Async.Task.CompletedTask;
}

View File

@ -8,14 +8,14 @@ namespace Microsoft.OneFuzz.Service;
public class QueueNodeHearbeat
{
private readonly ILogTracerFactory _loggerFactory;
private readonly ILogTracer _log;
private readonly IEvents _events;
private readonly INodeOperations _nodes;
public QueueNodeHearbeat(ILogTracerFactory loggerFactory, INodeOperations nodes, IEvents events)
public QueueNodeHearbeat(ILogTracer log, INodeOperations nodes, IEvents events)
{
_loggerFactory = loggerFactory;
_log = log;
_nodes = nodes;
_events = events;
}
@ -23,13 +23,14 @@ public class QueueNodeHearbeat
[Function("QueueNodeHearbeat")]
public async Async.Task Run([QueueTrigger("myqueue-items", Connection = "AzureWebJobsStorage")] string msg)
{
var log = _loggerFactory.MakeLogTracer(Guid.NewGuid());
log.Info($"heartbeat: {msg}");
_log.Info($"heartbeat: {msg}");
var hb = JsonSerializer.Deserialize<NodeHeartbeatEntry>(msg, EntityConverter.GetJsonSerializerOptions()).EnsureNotNull($"wrong data {msg}");
var node = await _nodes.GetByMachineId(hb.NodeId);
var log = _log.WithTag("NodeId", hb.NodeId.ToString());
if (node == null)
{
log.Warning($"invalid node id: {hb.NodeId}");
@ -38,8 +39,15 @@ public class QueueNodeHearbeat
var newNode = node with { Heartbeat = DateTimeOffset.UtcNow };
await _nodes.Replace(newNode);
var r = await _nodes.Replace(newNode);
if (!r.IsOk)
{
var (status, reason) = r.ErrorV;
log.Error($"Failed to replace heartbeat info due to [{status}] {reason}");
}
// TODO: do we still send event if we fail do update the table ?
await _events.SendEvent(new EventNodeHeartbeat(node.MachineId, node.ScalesetId, node.PoolName));
}
}

View File

@ -7,36 +7,40 @@ namespace Microsoft.OneFuzz.Service;
public class QueueProxyHearbeat
{
private readonly ILogTracerFactory _loggerFactory;
private readonly ILogTracer _log;
private readonly IProxyOperations _proxy;
public QueueProxyHearbeat(ILogTracerFactory loggerFactory, IProxyOperations proxy)
public QueueProxyHearbeat(ILogTracer log, IProxyOperations proxy)
{
_loggerFactory = loggerFactory;
_log = log;
_proxy = proxy;
}
[Function("QueueProxyHearbeat")]
public async Async.Task Run([QueueTrigger("myqueue-items", Connection = "AzureWebJobsStorage")] string msg)
{
var log = _loggerFactory.MakeLogTracer(Guid.NewGuid());
log.Info($"heartbeat: {msg}");
_log.Info($"heartbeat: {msg}");
var hb = JsonSerializer.Deserialize<ProxyHeartbeat>(msg, EntityConverter.GetJsonSerializerOptions()).EnsureNotNull($"wrong data {msg}"); ;
var newHb = hb with { TimeStamp = DateTimeOffset.UtcNow };
var proxy = await _proxy.GetByProxyId(newHb.ProxyId);
var log = _log.WithTag("ProxyId", newHb.ProxyId.ToString());
if (proxy == null)
{
log.AddTags(new[] { ("Proxy ID", newHb.ProxyId.ToString()) }).Warning($"invalid proxy id: {newHb.ProxyId}");
log.Warning($"invalid proxy id: {newHb.ProxyId}");
return;
}
var newProxy = proxy with { heartbeat = newHb };
await _proxy.Replace(newProxy);
var r = await _proxy.Replace(newProxy);
if (!r.IsOk)
{
var (status, reason) = r.ErrorV;
log.Error($"Failed to replace proxy heartbeat record due to [{status}] {reason}");
}
}
}

View File

@ -0,0 +1,65 @@
using System;
using System.Net;
using System.Threading.Tasks;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
namespace Microsoft.OneFuzz.Service;
public record FunctionInfo(string Name, string ResourceGroup, string? SlotName);
public class TestHooks
{
private readonly ILogTracer _log;
private readonly IConfigOperations _configOps;
private readonly IEvents _events;
public TestHooks(ILogTracer log, IConfigOperations configOps, IEvents events)
{
_log = log;
_configOps = configOps;
_events = events;
}
[Function("Info")]
public async Task<HttpResponseData> Info([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "testhooks/info")] HttpRequestData req)
{
_log.Info("Creating function info response");
var response = req.CreateResponse();
FunctionInfo info = new(
$"{EnvironmentVariables.OneFuzz.InstanceName}",
$"{EnvironmentVariables.OneFuzz.ResourceGroup}",
Environment.GetEnvironmentVariable("WEBSITE_SLOT_NAME"));
_log.Info("Returning function info");
await response.WriteAsJsonAsync(info);
_log.Info("Returned function info");
return response;
}
[Function("InstanceConfig")]
public async Task<HttpResponseData> InstanceConfig([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "testhooks/instance-config")] HttpRequestData req)
{
_log.Info("Fetching instance config");
var config = await _configOps.Fetch();
if (config is null)
{
_log.Error("Instance config is null");
Error err = new(ErrorCode.INVALID_REQUEST, new[] { "Instance config is null" });
var resp = req.CreateResponse(HttpStatusCode.InternalServerError);
await resp.WriteAsJsonAsync(err);
return resp;
}
else
{
await _events.SendEvent(new EventInstanceConfigUpdated(config));
var resp = req.CreateResponse(HttpStatusCode.OK);
await resp.WriteAsJsonAsync(config);
return resp;
}
}
}

View File

@ -16,7 +16,6 @@ namespace Microsoft.OneFuzz.Service
);
public interface IEvents
{
public Async.Task SendEvent(BaseEvent anEvent);
@ -27,14 +26,14 @@ namespace Microsoft.OneFuzz.Service
public class Events : IEvents
{
private readonly IQueue _queue;
private readonly ILogTracerFactory _loggerFactory;
private readonly IWebhookOperations _webhook;
private ILogTracer _log;
public Events(IQueue queue, ILogTracerFactory loggerFactory, IWebhookOperations webhook)
public Events(IQueue queue, IWebhookOperations webhook, ILogTracer log)
{
_queue = queue;
_loggerFactory = loggerFactory;
_webhook = webhook;
_log = log;
}
public async Async.Task QueueSignalrEvent(EventMessage eventMessage)
@ -46,7 +45,6 @@ namespace Microsoft.OneFuzz.Service
public async Async.Task SendEvent(BaseEvent anEvent)
{
var log = _loggerFactory.MakeLogTracer(Guid.NewGuid());
var eventType = anEvent.GetEventType();
var eventMessage = new EventMessage(
@ -58,16 +56,16 @@ namespace Microsoft.OneFuzz.Service
);
await QueueSignalrEvent(eventMessage);
await _webhook.SendEvent(eventMessage);
LogEvent(log, anEvent, eventType);
LogEvent(anEvent, eventType);
}
public void LogEvent(ILogTracer log, BaseEvent anEvent, EventType eventType)
public void LogEvent(BaseEvent anEvent, EventType eventType)
{
var options = EntityConverter.GetJsonSerializerOptions();
options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
options.Converters.Add(new RemoveUserInfo());
var serializedEvent = JsonSerializer.Serialize(anEvent, options);
log.AddTags(new[] { ("Event Type", eventType.ToString()) }).Info($"sending event: {eventType} - {serializedEvent}");
_log.WithTag("Event Type", eventType.ToString()).Info($"sending event: {eventType} - {serializedEvent}");
}
}

View File

@ -0,0 +1,65 @@
using ApiService.OneFuzzLib.Orm;
using System;
using System.Threading.Tasks;
namespace Microsoft.OneFuzz.Service;
public interface IConfigOperations : IOrm<InstanceConfig>
{
Task<InstanceConfig> Fetch();
Async.Task Save(InstanceConfig config, bool isNew, bool requireEtag);
}
public class ConfigOperations : Orm<InstanceConfig>, IConfigOperations
{
private readonly IEvents _events;
private readonly ILogTracer _log;
public ConfigOperations(IStorage storage, IEvents events, ILogTracer log) : base(storage)
{
_events = events;
_log = log;
}
public async Task<InstanceConfig> Fetch()
{
var key = EnvironmentVariables.OneFuzz.InstanceName ?? throw new Exception("Environment variable ONEFUZZ_INSTANCE_NAME is not set");
var config = await GetEntityAsync(key, key);
return config;
}
public async Async.Task Save(InstanceConfig config, bool isNew = false, bool requireEtag = false)
{
ResultOk<(int, string)> r;
if (isNew)
{
r = await Insert(config);
if (!r.IsOk)
{
var (status, reason) = r.ErrorV;
_log.Error($"Failed to save new instance config record with result [{status}] {reason}");
}
}
else if (requireEtag && config.ETag.HasValue)
{
r = await Update(config);
if (!r.IsOk)
{
var (status, reason) = r.ErrorV;
_log.Error($"Failed to update instance config record with result: [{status}] {reason}");
}
}
else
{
r = await Replace(config);
if (!r.IsOk)
{
var (status, reason) = r.ErrorV;
_log.Error($"Failed to replace instance config record with result [{status}] {reason}");
}
}
await _events.SendEvent(new EventInstanceConfigUpdated(config));
}
}

View File

@ -11,12 +11,12 @@ public interface IProxyOperations : IOrm<Proxy>
}
public class ProxyOperations : Orm<Proxy>, IProxyOperations
{
private readonly ILogTracerFactory _logger;
private readonly ILogTracer _log;
public ProxyOperations(ILogTracerFactory loggerFactory, IStorage storage)
public ProxyOperations(ILogTracer log, IStorage storage)
: base(storage)
{
_logger = loggerFactory;
_log = log;
}
public async Task<Proxy?> GetByProxyId(Guid proxyId)

View File

@ -16,12 +16,12 @@ public interface IQueue
public class Queue : IQueue
{
IStorage _storage;
ILogTracerFactory _loggerFactory;
ILogTracer _log;
public Queue(IStorage storage, ILogTracerFactory loggerFactory)
public Queue(IStorage storage, ILogTracer log)
{
_storage = storage;
_loggerFactory = loggerFactory;
_log = log;
}

View File

@ -18,7 +18,7 @@ public interface IStorage
{
public ArmClient GetMgmtClient();
public IEnumerable<string> CorpusAccounts(ILogTracer log);
public IEnumerable<string> CorpusAccounts();
string GetPrimaryAccount(StorageType storageType);
public (string?, string?) GetStorageAccountNameAndKey(string accountId);
}
@ -27,11 +27,13 @@ public class Storage : IStorage
{
private ICreds _creds;
private ArmClient _armClient;
private ILogTracer _log;
public Storage(ICreds creds)
public Storage(ICreds creds, ILogTracer log)
{
_creds = creds;
_armClient = new ArmClient(credential: _creds.GetIdentity(), defaultSubscriptionId: _creds.GetSubcription());
_log = log;
}
public static string GetFuncStorage()
@ -52,7 +54,7 @@ public class Storage : IStorage
}
// TODO: @cached
public IEnumerable<string> CorpusAccounts(ILogTracer log)
public IEnumerable<string> CorpusAccounts()
{
var skip = GetFuncStorage();
var results = new List<string> { GetFuzzStorage() };
@ -89,7 +91,7 @@ public class Storage : IStorage
results.Add(account.Id!);
}
log.Info($"corpus accounts: {JsonSerializer.Serialize(results)}");
_log.Info($"corpus accounts: {JsonSerializer.Serialize(results)}");
return results;
}

View File

@ -20,17 +20,16 @@ public class WebhookMessageLogOperations : Orm<WebhookMessageLog>, IWebhookMessa
);
private readonly IQueue _queue;
private readonly ILogTracerFactory _loggerFactory;
public WebhookMessageLogOperations(IStorage storage, IQueue queue, ILogTracerFactory loggerFactory) : base(storage)
private readonly ILogTracer _log;
public WebhookMessageLogOperations(IStorage storage, IQueue queue, ILogTracer log) : base(storage)
{
_queue = queue;
_loggerFactory = loggerFactory;
_log = log;
}
public async Async.Task QueueWebhook(WebhookMessageLog webhookLog)
{
var log = _loggerFactory.MakeLogTracer(Guid.NewGuid());
var obj = new WebhookMessageQueueObj(webhookLog.WebhookId, webhookLog.EventId);
TimeSpan? visibilityTimeout = webhookLog.State switch
@ -42,7 +41,7 @@ public class WebhookMessageLogOperations : Orm<WebhookMessageLog>, IWebhookMessa
if (visibilityTimeout == null)
{
log.AddTags(
_log.WithTags(
new[] {
("WebhookId", webhookLog.WebhookId.ToString()),
("EventId", webhookLog.EventId.ToString()) }
@ -70,10 +69,12 @@ public interface IWebhookOperations
public class WebhookOperations : Orm<Webhook>, IWebhookOperations
{
private readonly IWebhookMessageLogOperations _webhookMessageLogOperations;
public WebhookOperations(IStorage storage, IWebhookMessageLogOperations webhookMessageLogOperations)
private readonly ILogTracer _log;
public WebhookOperations(IStorage storage, IWebhookMessageLogOperations webhookMessageLogOperations, ILogTracer log)
: base(storage)
{
_webhookMessageLogOperations = webhookMessageLogOperations;
_log = log;
}
async public Async.Task SendEvent(EventMessage eventMessage)
@ -99,7 +100,12 @@ public class WebhookOperations : Orm<Webhook>, IWebhookOperations
WebhookId: webhook.WebhookId
);
await _webhookMessageLogOperations.Replace(message);
var r = await _webhookMessageLogOperations.Replace(message);
if (!r.IsOk)
{
var (status, reason) = r.ErrorV;
_log.Error($"Failed to replace webhook message log due to [{status}] {reason}");
}
}

View File

@ -198,10 +198,16 @@ public class EntityConverter
{
throw new Exception("invalid ");
}
}
var fieldName = ef.columnName;
var obj = entity[fieldName];
if (obj == null)
{
return null;
}
var objType = obj.GetType();
if (ef.type == typeof(string))
{
return entity.GetString(fieldName);
@ -245,10 +251,6 @@ public class EntityConverter
else
{
var value = entity.GetString(fieldName);
if (value == null)
{
return null;
}
return JsonSerializer.Deserialize(value, ef.type, options: _options); ;
}
}

View File

@ -12,7 +12,10 @@ namespace ApiService.OneFuzzLib.Orm
{
Task<TableClient> GetTableClient(string table, string? accountId = null);
IAsyncEnumerable<T> QueryAsync(string filter);
Task<bool> Replace(T entity);
Task<ResultOk<(int, string)>> Replace(T entity);
Task<T> GetEntityAsync(string partitionKey, string rowKey);
Task<ResultOk<(int, string)>> Insert(T entity);
}
public class Orm<T> : IOrm<T> where T : EntityBase
@ -36,13 +39,65 @@ namespace ApiService.OneFuzzLib.Orm
}
}
public async Task<bool> Replace(T entity)
public async Task<ResultOk<(int, string)>> Insert(T entity)
{
var tableClient = await GetTableClient(typeof(T).Name);
var tableEntity = _entityConverter.ToTableEntity(entity);
var response = await tableClient.AddEntityAsync(tableEntity);
if (response.IsError)
{
return ResultOk<(int, string)>.Error((response.Status, response.ReasonPhrase));
}
else
{
return ResultOk<(int, string)>.Ok();
}
}
public async Task<ResultOk<(int, string)>> Replace(T entity)
{
var tableClient = await GetTableClient(typeof(T).Name);
var tableEntity = _entityConverter.ToTableEntity(entity);
var response = await tableClient.UpsertEntityAsync(tableEntity);
return !response.IsError;
if (response.IsError)
{
return ResultOk<(int, string)>.Error((response.Status, response.ReasonPhrase));
}
else
{
return ResultOk<(int, string)>.Ok();
}
}
public async Task<ResultOk<(int, string)>> Update(T entity)
{
var tableClient = await GetTableClient(typeof(T).Name);
var tableEntity = _entityConverter.ToTableEntity(entity);
if (entity.ETag is null)
{
return ResultOk<(int, string)>.Error((0, "ETag must be set when updating an entity"));
}
else
{
var response = await tableClient.UpdateEntityAsync(tableEntity, entity.ETag.Value);
if (response.IsError)
{
return ResultOk<(int, string)>.Error((response.Status, response.ReasonPhrase));
}
else
{
return ResultOk<(int, string)>.Ok();
}
}
}
public async Task<T> GetEntityAsync(string partitionKey, string rowKey)
{
var tableClient = await GetTableClient(typeof(T).Name);
var tableEntity = await tableClient.GetEntityAsync<TableEntity>(partitionKey, rowKey);
return _entityConverter.ToRecord<T>(tableEntity);
}
public async Task<TableClient> GetTableClient(string table, string? accountId = null)

View File

@ -214,6 +214,7 @@ module pythonFunction 'bicep-templates/function.bicep' = {
functions_extension_version: '~3'
name: name
instance_name: name
app_logs_sas_url: storage.outputs.FuncSasUrlBlobAppLogs
app_func_audiences: app_func_audiences
app_func_issuer: app_func_issuer
@ -244,6 +245,7 @@ module netFunction 'bicep-templates/function.bicep' = {
functions_extension_version: '~4'
name: '${name}-net'
instance_name: name
app_logs_sas_url: storage.outputs.FuncSasUrlBlobAppLogs
app_func_audiences: app_func_audiences
app_func_issuer: app_func_issuer

View File

@ -1,4 +1,5 @@
param name string
param instance_name string
param location string
param owner string
@ -137,7 +138,7 @@ resource pythonFunctionSettings 'Microsoft.Web/sites/config@2021-03-01' = {
'AzureWebJobsDisableHomepage': 'true'
'AzureSignalRConnectionString': signal_r_connection_string
'AzureSignalRServiceTransportType': 'Transient'
'ONEFUZZ_INSTANCE_NAME': name
'ONEFUZZ_INSTANCE_NAME': instance_name
'ONEFUZZ_INSTANCE': 'https://${name}.azurewebsites.net'
'ONEFUZZ_RESOURCE_GROUP': resourceGroup().id
'ONEFUZZ_DATA_STORAGE': fuzz_storage_resource_id