diff --git a/src/ApiService/ApiService/ApiService.csproj b/src/ApiService/ApiService/ApiService.csproj
index 1b1fb1a06..ec2bc65a7 100644
--- a/src/ApiService/ApiService/ApiService.csproj
+++ b/src/ApiService/ApiService/ApiService.csproj
@@ -5,6 +5,7 @@
true
Exe
enable
+ 5
diff --git a/src/ApiService/ApiService/EnvironmentVariables.cs b/src/ApiService/ApiService/EnvironmentVariables.cs
index 9b406ff27..1fee50cef 100644
--- a/src/ApiService/ApiService/EnvironmentVariables.cs
+++ b/src/ApiService/ApiService/EnvironmentVariables.cs
@@ -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"); }
diff --git a/src/ApiService/ApiService/Info.cs b/src/ApiService/ApiService/Info.cs
deleted file mode 100644
index 9676ed51b..000000000
--- a/src/ApiService/ApiService/Info.cs
+++ /dev/null
@@ -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 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;
- }
-
-}
diff --git a/src/ApiService/ApiService/Log.cs b/src/ApiService/ApiService/Log.cs
index acd26f354..2a4940c7e 100644
--- a/src/ApiService/ApiService/Log.cs
+++ b/src/ApiService/ApiService/Log.cs
@@ -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 _loggers;
+ private Dictionary _tags;
+ private SeverityLevel _logSeverityLevel;
- public Guid CorrelationId { get; }
- public IReadOnlyDictionary Tags { get; }
-
+ public Guid CorrelationId => _correlationId;
+ public IReadOnlyDictionary Tags => _tags;
private static List> ConvertTags((string, string)[]? tags)
{
@@ -166,17 +178,43 @@ public class LogTracer : ILogTracer
}
}
- public LogTracer(Guid correlationId, (string, string)[]? tags, List loggers) : this(correlationId, new Dictionary(ConvertTags(tags)), loggers) { }
+ public LogTracer(Guid correlationId, (string, string)[]? tags, List loggers, SeverityLevel logSeverityLevel) :
+ this(correlationId, new Dictionary(ConvertTags(tags)), loggers, logSeverityLevel)
+ { }
- public LogTracer(Guid correlationId, IReadOnlyDictionary tags, List loggers)
+ public LogTracer(Guid correlationId, IReadOnlyDictionary tags, List 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(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);
}
}
diff --git a/src/ApiService/ApiService/OneFuzzTypes/Enums.cs b/src/ApiService/ApiService/OneFuzzTypes/Enums.cs
index 45d7876a1..401fbe55e 100644
--- a/src/ApiService/ApiService/OneFuzzTypes/Enums.cs
+++ b/src/ApiService/ApiService/OneFuzzTypes/Enums.cs
@@ -1,4 +1,5 @@
-public enum ErrorCode
+namespace Microsoft.OneFuzz.Service;
+public enum ErrorCode
{
INVALID_REQUEST = 450,
INVALID_PERMISSION = 451,
diff --git a/src/ApiService/ApiService/OneFuzzTypes/Events.cs b/src/ApiService/ApiService/OneFuzzTypes/Events.cs
index 6f628b1b3..9fc68caac 100644
--- a/src/ApiService/ApiService/OneFuzzTypes/Events.cs
+++ b/src/ApiService/ApiService/OneFuzzTypes/Events.cs
@@ -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();
}
diff --git a/src/ApiService/ApiService/OneFuzzTypes/Model.cs b/src/ApiService/ApiService/OneFuzzTypes/Model.cs
index 346431a4d..467a60c07 100644
--- a/src/ApiService/ApiService/OneFuzzTypes/Model.cs
+++ b/src/ApiService/ApiService/OneFuzzTypes/Model.cs
@@ -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 Events { get; set; } = new List();
List Nodes { get; set; } = new List();
+}
+public record AzureSecurityExtensionConfig();
+public record GenevaExtensionConfig();
-}
\ No newline at end of file
+
+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(), Array.Empty()) { }
+}
+
+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? ApiAccessRules,
+ IDictionary? GroupMembership,
+
+ IDictionary? VmTags,
+ IDictionary? VmssTags
+) : EntityBase()
+{
+ public InstanceConfig(string instanceName) : this(
+ instanceName,
+ null,
+ true,
+ Array.Empty(),
+ new NetworkConfig(),
+ new NetworkSecurityGroupConfig(),
+ null,
+ "Standard_B2s",
+ null,
+ null,
+ null,
+ null)
+ { }
+
+ public List? CheckAdmins(List? 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> CheckInstanceConfig()
+ {
+ List errors = new();
+ if (AllowedAadTenants.Length == 0)
+ {
+ errors.Add("allowed_aad_tenants must not be empty");
+ }
+ if (errors.Count == 0)
+ {
+ return ResultOk>.Ok();
+ }
+ else
+ {
+ return ResultOk>.Error(errors);
+ }
+ }
+}
diff --git a/src/ApiService/ApiService/OneFuzzTypes/ReturnTypes.cs b/src/ApiService/ApiService/OneFuzzTypes/ReturnTypes.cs
index c77358b96..6651851b0 100644
--- a/src/ApiService/ApiService/OneFuzzTypes/ReturnTypes.cs
+++ b/src/ApiService/ApiService/OneFuzzTypes/ReturnTypes.cs
@@ -1,6 +1,24 @@
namespace Microsoft.OneFuzz.Service
{
+ public struct ResultOk
+ {
+ public static ResultOk Ok() => new();
+ public static ResultOk 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
{
public static Result 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;
}
diff --git a/src/ApiService/ApiService/Program.cs b/src/ApiService/ApiService/Program.cs
index 02fd6b386..9a707d23e 100644
--- a/src/ApiService/ApiService/Program.cs
+++ b/src/ApiService/ApiService/Program.cs
@@ -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();
+ if (log is not null)
+ {
+ log.AddTags(new[] {
+ ("InvocationId", context.InvocationId.ToString())
+
+ });
+ }
+
+ await next(context);
+ }
+ }
+
+
public static List GetLoggers()
{
List loggers = new List();
@@ -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();
+ }
+ )
.ConfigureServices((context, services) =>
services
- .AddSingleton(_ => new LogTracerFactory(GetLoggers()))
+ .AddScoped(_ => new LogTracerFactory(GetLoggers()).CreateLogTracer(Guid.NewGuid(), severityLevel: EnvironmentVariables.LogSeverityLevel()))
.AddSingleton()
.AddSingleton()
.AddSingleton()
.AddSingleton()
.AddSingleton()
.AddSingleton()
- .AddSingleton(_ => new Creds())
+ .AddSingleton()
.AddSingleton()
.AddSingleton()
+ .AddSingleton()
)
.Build();
diff --git a/src/ApiService/ApiService/QueueFileChanges.cs b/src/ApiService/ApiService/QueueFileChanges.cs
index c90b3b977..c106d986e 100644
--- a/src/ApiService/ApiService/QueueFileChanges.cs
+++ b/src/ApiService/ApiService/QueueFileChanges.cs
@@ -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>(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;
}
diff --git a/src/ApiService/ApiService/QueueNodeHearbeat.cs b/src/ApiService/ApiService/QueueNodeHearbeat.cs
index c7ea5ea8c..b0425d8c1 100644
--- a/src/ApiService/ApiService/QueueNodeHearbeat.cs
+++ b/src/ApiService/ApiService/QueueNodeHearbeat.cs
@@ -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(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));
}
}
diff --git a/src/ApiService/ApiService/QueueProxyHeartbeat.cs b/src/ApiService/ApiService/QueueProxyHeartbeat.cs
index 9eb437d7b..fb053f695 100644
--- a/src/ApiService/ApiService/QueueProxyHeartbeat.cs
+++ b/src/ApiService/ApiService/QueueProxyHeartbeat.cs
@@ -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(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}");
+ }
}
}
diff --git a/src/ApiService/ApiService/TestHooks.cs b/src/ApiService/ApiService/TestHooks.cs
new file mode 100644
index 000000000..7a24131ea
--- /dev/null
+++ b/src/ApiService/ApiService/TestHooks.cs
@@ -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 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 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;
+ }
+ }
+}
diff --git a/src/ApiService/ApiService/onefuzzlib/Events.cs b/src/ApiService/ApiService/onefuzzlib/Events.cs
index 1efe316a6..7c28016b3 100644
--- a/src/ApiService/ApiService/onefuzzlib/Events.cs
+++ b/src/ApiService/ApiService/onefuzzlib/Events.cs
@@ -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}");
}
}
diff --git a/src/ApiService/ApiService/onefuzzlib/InstanceConfig.cs b/src/ApiService/ApiService/onefuzzlib/InstanceConfig.cs
new file mode 100644
index 000000000..66ba7e72d
--- /dev/null
+++ b/src/ApiService/ApiService/onefuzzlib/InstanceConfig.cs
@@ -0,0 +1,65 @@
+using ApiService.OneFuzzLib.Orm;
+using System;
+using System.Threading.Tasks;
+
+namespace Microsoft.OneFuzz.Service;
+
+
+public interface IConfigOperations : IOrm
+{
+ Task Fetch();
+
+ Async.Task Save(InstanceConfig config, bool isNew, bool requireEtag);
+}
+
+public class ConfigOperations : Orm, 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 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));
+ }
+}
diff --git a/src/ApiService/ApiService/onefuzzlib/ProxyOperations.cs b/src/ApiService/ApiService/onefuzzlib/ProxyOperations.cs
index 358e28eb3..e6bcbe0c6 100644
--- a/src/ApiService/ApiService/onefuzzlib/ProxyOperations.cs
+++ b/src/ApiService/ApiService/onefuzzlib/ProxyOperations.cs
@@ -11,12 +11,12 @@ public interface IProxyOperations : IOrm
}
public class ProxyOperations : Orm, 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 GetByProxyId(Guid proxyId)
diff --git a/src/ApiService/ApiService/onefuzzlib/Queue.cs b/src/ApiService/ApiService/onefuzzlib/Queue.cs
index 3df5f1785..5b0f82324 100644
--- a/src/ApiService/ApiService/onefuzzlib/Queue.cs
+++ b/src/ApiService/ApiService/onefuzzlib/Queue.cs
@@ -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;
}
diff --git a/src/ApiService/ApiService/onefuzzlib/Storage.cs b/src/ApiService/ApiService/onefuzzlib/Storage.cs
index 254c3a28d..ffafb282b 100644
--- a/src/ApiService/ApiService/onefuzzlib/Storage.cs
+++ b/src/ApiService/ApiService/onefuzzlib/Storage.cs
@@ -18,7 +18,7 @@ public interface IStorage
{
public ArmClient GetMgmtClient();
- public IEnumerable CorpusAccounts(ILogTracer log);
+ public IEnumerable 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 CorpusAccounts(ILogTracer log)
+ public IEnumerable CorpusAccounts()
{
var skip = GetFuncStorage();
var results = new List { 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;
}
diff --git a/src/ApiService/ApiService/onefuzzlib/WebhookOperations.cs b/src/ApiService/ApiService/onefuzzlib/WebhookOperations.cs
index c30f66348..b5e760806 100644
--- a/src/ApiService/ApiService/onefuzzlib/WebhookOperations.cs
+++ b/src/ApiService/ApiService/onefuzzlib/WebhookOperations.cs
@@ -20,17 +20,16 @@ public class WebhookMessageLogOperations : Orm, 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, 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, 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, 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}");
+ }
}
diff --git a/src/ApiService/ApiService/onefuzzlib/orm/EntityConverter.cs b/src/ApiService/ApiService/onefuzzlib/orm/EntityConverter.cs
index bef3afa80..1fc1c2852 100644
--- a/src/ApiService/ApiService/onefuzzlib/orm/EntityConverter.cs
+++ b/src/ApiService/ApiService/onefuzzlib/orm/EntityConverter.cs
@@ -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); ;
}
}
diff --git a/src/ApiService/ApiService/onefuzzlib/orm/Orm.cs b/src/ApiService/ApiService/onefuzzlib/orm/Orm.cs
index 1cf9e781f..51bcd9392 100644
--- a/src/ApiService/ApiService/onefuzzlib/orm/Orm.cs
+++ b/src/ApiService/ApiService/onefuzzlib/orm/Orm.cs
@@ -12,7 +12,10 @@ namespace ApiService.OneFuzzLib.Orm
{
Task GetTableClient(string table, string? accountId = null);
IAsyncEnumerable QueryAsync(string filter);
- Task Replace(T entity);
+ Task> Replace(T entity);
+
+ Task GetEntityAsync(string partitionKey, string rowKey);
+ Task> Insert(T entity);
}
public class Orm : IOrm where T : EntityBase
@@ -36,13 +39,65 @@ namespace ApiService.OneFuzzLib.Orm
}
}
- public async Task Replace(T entity)
+ public async Task> 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> 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> 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 GetEntityAsync(string partitionKey, string rowKey)
+ {
+ var tableClient = await GetTableClient(typeof(T).Name);
+ var tableEntity = await tableClient.GetEntityAsync(partitionKey, rowKey);
+ return _entityConverter.ToRecord(tableEntity);
}
public async Task GetTableClient(string table, string? accountId = null)
diff --git a/src/deployment/azuredeploy.bicep b/src/deployment/azuredeploy.bicep
index cb4e41221..44bfea780 100644
--- a/src/deployment/azuredeploy.bicep
+++ b/src/deployment/azuredeploy.bicep
@@ -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
diff --git a/src/deployment/bicep-templates/function.bicep b/src/deployment/bicep-templates/function.bicep
index 337cce30a..c5f3b2beb 100644
--- a/src/deployment/bicep-templates/function.bicep
+++ b/src/deployment/bicep-templates/function.bicep
@@ -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