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