mirror of
https://github.com/microsoft/onefuzz.git
synced 2025-06-14 11:08:06 +00:00
Migrating notifications (#2188)
* Migrating notification * add dotnet enpoint setting in the config * format * fix unit test * format * build fix * fix notifictions function definition * fix deserilization of requests refactor secretdata finish transfering Notifiction objects * format
This commit is contained in:
84
src/ApiService/ApiService/Functions/Notifications.cs
Normal file
84
src/ApiService/ApiService/Functions/Notifications.cs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
using System.Net;
|
||||||
|
using Microsoft.Azure.Functions.Worker;
|
||||||
|
using Microsoft.Azure.Functions.Worker.Http;
|
||||||
|
|
||||||
|
namespace Microsoft.OneFuzz.Service.Functions;
|
||||||
|
|
||||||
|
public class Notifications {
|
||||||
|
private readonly ILogTracer _log;
|
||||||
|
private readonly IEndpointAuthorization _auth;
|
||||||
|
private readonly IOnefuzzContext _context;
|
||||||
|
|
||||||
|
public Notifications(ILogTracer log, IEndpointAuthorization auth, IOnefuzzContext context) {
|
||||||
|
_log = log;
|
||||||
|
_auth = auth;
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Async.Task<HttpResponseData> Get(HttpRequestData req) {
|
||||||
|
_log.Info("Notification search");
|
||||||
|
var request = await RequestHandling.ParseUri<NotificationSearch>(req);
|
||||||
|
if (!request.IsOk) {
|
||||||
|
return await _context.RequestHandling.NotOk(req, request.ErrorV, "notification search");
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries = request.OkV switch { { Container: null } => _context.NotificationOperations.SearchAll(), { Container: var c } => _context.NotificationOperations.SearchByRowKeys(c.Select(x => x.ContainerName))
|
||||||
|
};
|
||||||
|
var response = req.CreateResponse(HttpStatusCode.OK);
|
||||||
|
await response.WriteAsJsonAsync(entries);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async Async.Task<HttpResponseData> Post(HttpRequestData req) {
|
||||||
|
_log.Info("adding notification hook");
|
||||||
|
var request = await RequestHandling.ParseRequest<NotificationCreate>(req);
|
||||||
|
if (!request.IsOk) {
|
||||||
|
return await _context.RequestHandling.NotOk(req, request.ErrorV, "notification create");
|
||||||
|
}
|
||||||
|
|
||||||
|
var notificationRequest = request.OkV;
|
||||||
|
|
||||||
|
var entry = await _context.NotificationOperations.Create(notificationRequest.Container, notificationRequest.Config,
|
||||||
|
notificationRequest.ReplaceExisting);
|
||||||
|
|
||||||
|
if (!entry.IsOk) {
|
||||||
|
return await _context.RequestHandling.NotOk(req, entry.ErrorV, context: "notification create");
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = req.CreateResponse(HttpStatusCode.OK);
|
||||||
|
await response.WriteAsJsonAsync(entry.OkV);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Async.Task<HttpResponseData> Delete(HttpRequestData req) {
|
||||||
|
var request = await RequestHandling.ParseRequest<NotificationGet>(req);
|
||||||
|
|
||||||
|
if (!request.IsOk) {
|
||||||
|
return await _context.RequestHandling.NotOk(req, request.ErrorV, context: "notification delete");
|
||||||
|
}
|
||||||
|
var entries = await _context.NotificationOperations.SearchByPartitionKeys(new[] { $"{request.OkV.NotificationId}" }).ToListAsync();
|
||||||
|
|
||||||
|
if (entries.Count == 0) {
|
||||||
|
return await _context.RequestHandling.NotOk(req, new Error(ErrorCode.INVALID_REQUEST, new[] { "unable to find notification" }), context: "notification delete");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entries.Count > 1) {
|
||||||
|
return await _context.RequestHandling.NotOk(req, new Error(ErrorCode.INVALID_REQUEST, new[] { "error identifying Notification" }), context: "notification delete");
|
||||||
|
}
|
||||||
|
var response = req.CreateResponse(HttpStatusCode.OK);
|
||||||
|
await response.WriteAsJsonAsync(entries[0]);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Function("Notifications")]
|
||||||
|
public Async.Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymous, "GET", "POST", "DELETE")] HttpRequestData req) {
|
||||||
|
return _auth.CallIfUser(req, r => r.Method switch {
|
||||||
|
"GET" => Get(r),
|
||||||
|
"POST" => Post(r),
|
||||||
|
"DELETE" => Delete(r),
|
||||||
|
_ => throw new InvalidOperationException("Unsupported HTTP method"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -308,3 +308,14 @@ public enum NodeDisposalStrategy {
|
|||||||
ScaleIn,
|
ScaleIn,
|
||||||
Decomission
|
Decomission
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public enum GithubIssueState {
|
||||||
|
Open,
|
||||||
|
Closed
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum GithubIssueSearchMatch {
|
||||||
|
Title,
|
||||||
|
Body
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json;
|
using System.Reflection;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Microsoft.OneFuzz.Service.OneFuzzLib.Orm;
|
using Microsoft.OneFuzz.Service.OneFuzzLib.Orm;
|
||||||
using Endpoint = System.String;
|
using Endpoint = System.String;
|
||||||
@ -402,6 +403,9 @@ public record Scaleset(
|
|||||||
[JsonConverter(typeof(ContainerConverter))]
|
[JsonConverter(typeof(ContainerConverter))]
|
||||||
public record Container(string ContainerName) {
|
public record Container(string ContainerName) {
|
||||||
public string ContainerName { get; } = ContainerName.All(c => char.IsLetterOrDigit(c) || c == '-') ? ContainerName : throw new ArgumentException("Container name must have only numbers, letters or dashes");
|
public string ContainerName { get; } = ContainerName.All(c => char.IsLetterOrDigit(c) || c == '-') ? ContainerName : throw new ArgumentException("Container name must have only numbers, letters or dashes");
|
||||||
|
public override string ToString() {
|
||||||
|
return ContainerName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ContainerConverter : JsonConverter<Container> {
|
public class ContainerConverter : JsonConverter<Container> {
|
||||||
@ -416,8 +420,8 @@ public class ContainerConverter : JsonConverter<Container> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public record Notification(
|
public record Notification(
|
||||||
Container Container,
|
[PartitionKey] Guid NotificationId,
|
||||||
Guid NotificationId,
|
[RowKey] Container Container,
|
||||||
NotificationTemplate Config
|
NotificationTemplate Config
|
||||||
) : EntityBase();
|
) : EntityBase();
|
||||||
|
|
||||||
@ -469,17 +473,98 @@ public record RegressionReport(
|
|||||||
CrashTestResult? OriginalCrashTestResult
|
CrashTestResult? OriginalCrashTestResult
|
||||||
) : IReport;
|
) : IReport;
|
||||||
|
|
||||||
public record NotificationTemplate(
|
|
||||||
AdoTemplate? AdoTemplate,
|
[JsonConverter(typeof(NotificationTemplateConverter))]
|
||||||
TeamsTemplate? TeamsTemplate,
|
#pragma warning disable CA1715
|
||||||
GithubIssuesTemplate? GithubIssuesTemplate
|
public interface NotificationTemplate {
|
||||||
|
#pragma warning restore CA1715
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class NotificationTemplateConverter : JsonConverter<NotificationTemplate> {
|
||||||
|
public override NotificationTemplate? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
|
||||||
|
using var templateJson = JsonDocument.ParseValue(ref reader);
|
||||||
|
try {
|
||||||
|
return templateJson.Deserialize<AdoTemplate>(options);
|
||||||
|
} catch (JsonException) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return templateJson.Deserialize<TeamsTemplate>(options);
|
||||||
|
} catch (JsonException) {
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return templateJson.Deserialize<GithubIssuesTemplate>(options);
|
||||||
|
} catch (JsonException) {
|
||||||
|
}
|
||||||
|
throw new JsonException("Unsupported notification template");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, NotificationTemplate value, JsonSerializerOptions options) {
|
||||||
|
if (value is AdoTemplate adoTemplate) {
|
||||||
|
JsonSerializer.Serialize(writer, adoTemplate, options);
|
||||||
|
} else if (value is TeamsTemplate teamsTemplate) {
|
||||||
|
JsonSerializer.Serialize(writer, teamsTemplate, options);
|
||||||
|
} else if (value is GithubIssuesTemplate githubIssuesTemplate) {
|
||||||
|
JsonSerializer.Serialize(writer, githubIssuesTemplate, options);
|
||||||
|
} else {
|
||||||
|
throw new JsonException("Unsupported notification template");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public record ADODuplicateTemplate(
|
||||||
|
List<string> Increment,
|
||||||
|
string? Comment,
|
||||||
|
Dictionary<string, string> SetState,
|
||||||
|
Dictionary<string, string> AdoFields
|
||||||
);
|
);
|
||||||
|
|
||||||
public record AdoTemplate();
|
public record AdoTemplate(
|
||||||
|
Uri BaseUrl,
|
||||||
|
SecretData<string> AuthToken,
|
||||||
|
string Project,
|
||||||
|
string Type,
|
||||||
|
List<string> UniqueFields,
|
||||||
|
string? Comment,
|
||||||
|
Dictionary<string, string> AdoFields,
|
||||||
|
ADODuplicateTemplate OnDuplicate
|
||||||
|
) : NotificationTemplate;
|
||||||
|
|
||||||
public record TeamsTemplate();
|
public record TeamsTemplate(SecretData<string> Url) : NotificationTemplate;
|
||||||
|
|
||||||
public record GithubIssuesTemplate();
|
|
||||||
|
public record GithubAuth(string User, string PersonalAccessToken);
|
||||||
|
|
||||||
|
public record GithubIssueSearch(
|
||||||
|
string? Author,
|
||||||
|
GithubIssueState? State,
|
||||||
|
List<GithubIssueSearchMatch> FieldMatch,
|
||||||
|
[property: JsonPropertyName("string")] String str
|
||||||
|
);
|
||||||
|
|
||||||
|
public record GithubIssueDuplicate(
|
||||||
|
string? Comment,
|
||||||
|
List<string> Labels,
|
||||||
|
bool Reopen
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
public record GithubIssuesTemplate(
|
||||||
|
SecretData<GithubAuth> Auth,
|
||||||
|
string Organization,
|
||||||
|
string Repository,
|
||||||
|
string Title,
|
||||||
|
string Body,
|
||||||
|
GithubIssueSearch UniqueSearch,
|
||||||
|
List<string> Assignees,
|
||||||
|
List<string> Labels,
|
||||||
|
GithubIssueDuplicate OnDuplicate
|
||||||
|
) : NotificationTemplate;
|
||||||
|
|
||||||
public record Repro(
|
public record Repro(
|
||||||
[PartitionKey][RowKey] Guid VmId,
|
[PartitionKey][RowKey] Guid VmId,
|
||||||
@ -553,25 +638,57 @@ public record Vm(
|
|||||||
public string Name { get; } = Name.Length > 40 ? throw new ArgumentOutOfRangeException("VM name too long") : Name;
|
public string Name { get; } = Name.Length > 40 ? throw new ArgumentOutOfRangeException("VM name too long") : Name;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[JsonConverter(typeof(ISecretConverterFactory))]
|
||||||
|
public interface ISecret<T> { }
|
||||||
|
|
||||||
public record SecretAddress(Uri Url);
|
public class ISecretConverterFactory : JsonConverterFactory {
|
||||||
|
public override bool CanConvert(Type typeToConvert) {
|
||||||
|
return typeToConvert.IsGenericType && typeToConvert.Name == typeof(ISecret<string>).Name;
|
||||||
/// This class allows us to store some data that are intended to be secret
|
|
||||||
/// The secret field stores either the raw data or the address of that data
|
|
||||||
/// This class allows us to maintain backward compatibility with existing
|
|
||||||
/// NotificationTemplate classes
|
|
||||||
public record SecretData<T>(T Secret) {
|
|
||||||
public override string ToString() {
|
|
||||||
if (Secret is SecretAddress) {
|
|
||||||
if (Secret is null) {
|
|
||||||
return string.Empty;
|
|
||||||
} else {
|
|
||||||
return Secret.ToString()!;
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
return "[REDACTED]";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) {
|
||||||
|
var innerType = typeToConvert.GetGenericArguments().First();
|
||||||
|
return (JsonConverter)Activator.CreateInstance(
|
||||||
|
typeof(ISecretConverter<>).MakeGenericType(innerType),
|
||||||
|
BindingFlags.Instance | BindingFlags.Public,
|
||||||
|
binder: null,
|
||||||
|
args: Array.Empty<object?>(),
|
||||||
|
culture: null)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ISecretConverter<T> : JsonConverter<ISecret<T>> {
|
||||||
|
public override ISecret<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
|
||||||
|
|
||||||
|
using var secretJson = JsonDocument.ParseValue(ref reader);
|
||||||
|
|
||||||
|
if (secretJson.RootElement.ValueKind == JsonValueKind.String) {
|
||||||
|
return (ISecret<T>)new SecretValue<string>(secretJson.RootElement.GetString()!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (secretJson.RootElement.TryGetProperty("url", out var secretUrl)) {
|
||||||
|
return new SecretAddress<T>(new Uri(secretUrl.GetString()!));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SecretValue<T>(secretJson.Deserialize<T>(options)!);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, ISecret<T> value, JsonSerializerOptions options) {
|
||||||
|
if (value is SecretAddress<T> secretAddress) {
|
||||||
|
JsonSerializer.Serialize(writer, secretAddress, options);
|
||||||
|
} else if (value is SecretValue<T> secretValue) {
|
||||||
|
JsonSerializer.Serialize(writer, secretValue.Value, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public record SecretValue<T>(T Value) : ISecret<T>;
|
||||||
|
|
||||||
|
public record SecretAddress<T>(Uri Url) : ISecret<T>;
|
||||||
|
|
||||||
|
public record SecretData<T>(ISecret<T> Secret) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public record JobConfig(
|
public record JobConfig(
|
||||||
|
@ -113,6 +113,20 @@ public record ContainerDelete(
|
|||||||
IDictionary<string, string>? Metadata = null
|
IDictionary<string, string>? Metadata = null
|
||||||
) : BaseRequest;
|
) : BaseRequest;
|
||||||
|
|
||||||
|
public record NotificationCreate(
|
||||||
|
Container Container,
|
||||||
|
bool ReplaceExisting,
|
||||||
|
NotificationTemplate Config
|
||||||
|
) : BaseRequest;
|
||||||
|
|
||||||
|
public record NotificationSearch(
|
||||||
|
List<Container>? Container
|
||||||
|
) : BaseRequest;
|
||||||
|
|
||||||
|
public record NotificationGet(
|
||||||
|
Guid NotificationId
|
||||||
|
) : BaseRequest;
|
||||||
|
|
||||||
public record JobGet(
|
public record JobGet(
|
||||||
Guid JobId
|
Guid JobId
|
||||||
);
|
);
|
||||||
|
@ -48,7 +48,7 @@ public abstract class ValidatedStringConverter<T> : JsonConverter<T> where T : V
|
|||||||
|
|
||||||
[JsonConverter(typeof(Converter))]
|
[JsonConverter(typeof(Converter))]
|
||||||
public record PoolName : ValidatedString {
|
public record PoolName : ValidatedString {
|
||||||
private PoolName(string value) : base(value) {
|
public PoolName(string value) : base(value) {
|
||||||
// Debug.Assert(Check.IsAlnumDash(value));
|
// Debug.Assert(Check.IsAlnumDash(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ public class TestHooks {
|
|||||||
select new KeyValuePair<string, string>(Uri.UnescapeDataString(cs.Substring(0, i)), Uri.UnescapeDataString(cs.Substring(i + 1)));
|
select new KeyValuePair<string, string>(Uri.UnescapeDataString(cs.Substring(0, i)), Uri.UnescapeDataString(cs.Substring(i + 1)));
|
||||||
|
|
||||||
var qs = new Dictionary<string, string>(q);
|
var qs = new Dictionary<string, string>(q);
|
||||||
var d = await _secretOps.GetSecretStringValue(new SecretData<string>(qs["SecretName"]));
|
var d = await _secretOps.GetSecretStringValue(new SecretData<string>(new SecretValue<string>(qs["SecretName"])));
|
||||||
|
|
||||||
var resp = req.CreateResponse(HttpStatusCode.OK);
|
var resp = req.CreateResponse(HttpStatusCode.OK);
|
||||||
await resp.WriteAsJsonAsync(d);
|
await resp.WriteAsJsonAsync(d);
|
||||||
|
@ -8,6 +8,7 @@ public interface INotificationOperations : IOrm<Notification> {
|
|||||||
Async.Task NewFiles(Container container, string filename, bool failTaskOnTransientError);
|
Async.Task NewFiles(Container container, string filename, bool failTaskOnTransientError);
|
||||||
IAsyncEnumerable<Notification> GetNotifications(Container container);
|
IAsyncEnumerable<Notification> GetNotifications(Container container);
|
||||||
IAsyncEnumerable<(Task, IEnumerable<string>)> GetQueueTasks();
|
IAsyncEnumerable<(Task, IEnumerable<string>)> GetQueueTasks();
|
||||||
|
Async.Task<OneFuzzResult<Notification>> Create(Container container, NotificationTemplate config, bool replaceExisting);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NotificationOperations : Orm<Notification>, INotificationOperations {
|
public class NotificationOperations : Orm<Notification>, INotificationOperations {
|
||||||
@ -33,20 +34,20 @@ public class NotificationOperations : Orm<Notification>, INotificationOperations
|
|||||||
|
|
||||||
done.Add(notification.Config);
|
done.Add(notification.Config);
|
||||||
|
|
||||||
if (notification.Config.TeamsTemplate != null) {
|
if (notification.Config is TeamsTemplate teamsTemplate) {
|
||||||
NotifyTeams(notification.Config.TeamsTemplate, container, filename, reportOrRegression!);
|
NotifyTeams(teamsTemplate, container, filename, reportOrRegression!);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reportOrRegression == null) {
|
if (reportOrRegression == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notification.Config.AdoTemplate != null) {
|
if (notification.Config is AdoTemplate adoTemplate) {
|
||||||
NotifyAdo(notification.Config.AdoTemplate, container, filename, reportOrRegression, failTaskOnTransientError);
|
NotifyAdo(adoTemplate, container, filename, reportOrRegression, failTaskOnTransientError);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notification.Config.GithubIssuesTemplate != null) {
|
if (notification.Config is GithubIssuesTemplate githubIssuesTemplate) {
|
||||||
GithubIssue(notification.Config.GithubIssuesTemplate, container, filename, reportOrRegression);
|
GithubIssue(githubIssuesTemplate, container, filename, reportOrRegression);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,6 +87,26 @@ public class NotificationOperations : Orm<Notification>, INotificationOperations
|
|||||||
.Where(taskTuple => taskTuple.Item2 != null)!;
|
.Where(taskTuple => taskTuple.Item2 != null)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Async.Task<OneFuzzResult<Notification>> Create(Container container, NotificationTemplate config, bool replaceExisting) {
|
||||||
|
if (await _context.Containers.FindContainer(container, StorageType.Corpus) == null) {
|
||||||
|
return OneFuzzResult<Notification>.Error(ErrorCode.INVALID_REQUEST, errors: new[] { "invalid container" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replaceExisting) {
|
||||||
|
var existing = this.SearchByRowKeys(new[] { container.ContainerName });
|
||||||
|
await foreach (var existingEntry in existing) {
|
||||||
|
_logTracer.Info($"replacing existing notification: {existingEntry.NotificationId} - {container}");
|
||||||
|
await this.Delete(existingEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var entry = new Notification(Guid.NewGuid(), container, config);
|
||||||
|
await this.Insert(entry);
|
||||||
|
_logTracer.Info($"created notification. notification_id:{entry.NotificationId} container:{entry.Container}");
|
||||||
|
|
||||||
|
return OneFuzzResult<Notification>.Ok(entry);
|
||||||
|
}
|
||||||
|
|
||||||
public async Async.Task<Task?> GetRegressionReportTask(RegressionReport report) {
|
public async Async.Task<Task?> GetRegressionReportTask(RegressionReport report) {
|
||||||
if (report.CrashTestResult.CrashReport != null) {
|
if (report.CrashTestResult.CrashReport != null) {
|
||||||
return await _context.TaskOperations.GetByJobIdAndTaskId(report.CrashTestResult.CrashReport.JobId, report.CrashTestResult.CrashReport.TaskId);
|
return await _context.TaskOperations.GetByJobIdAndTaskId(report.CrashTestResult.CrashReport.JobId, report.CrashTestResult.CrashReport.TaskId);
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using Faithlife.Utility;
|
||||||
using Microsoft.Azure.Functions.Worker.Http;
|
using Microsoft.Azure.Functions.Worker.Http;
|
||||||
|
using Microsoft.OneFuzz.Service.OneFuzzLib.Orm;
|
||||||
|
|
||||||
namespace Microsoft.OneFuzz.Service;
|
namespace Microsoft.OneFuzz.Service;
|
||||||
|
|
||||||
@ -29,7 +33,7 @@ public class RequestHandling : IRequestHandling {
|
|||||||
public static async Async.Task<OneFuzzResult<T>> ParseRequest<T>(HttpRequestData req) {
|
public static async Async.Task<OneFuzzResult<T>> ParseRequest<T>(HttpRequestData req) {
|
||||||
Exception? exception = null;
|
Exception? exception = null;
|
||||||
try {
|
try {
|
||||||
var t = await req.ReadFromJsonAsync<T>();
|
var t = await JsonSerializer.DeserializeAsync<T>(req.Body, EntityConverter.GetJsonSerializerOptions());
|
||||||
if (t != null) {
|
if (t != null) {
|
||||||
return OneFuzzResult<T>.Ok(t);
|
return OneFuzzResult<T>.Ok(t);
|
||||||
}
|
}
|
||||||
@ -46,6 +50,28 @@ public class RequestHandling : IRequestHandling {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Async.Task<OneFuzzResult<T>> ParseUri<T>(HttpRequestData req) {
|
||||||
|
var query = System.Web.HttpUtility.ParseQueryString(req.Url.Query);
|
||||||
|
var doc = new JsonObject();
|
||||||
|
foreach (var key in query.AllKeys.WhereNotNull()) {
|
||||||
|
doc[key] = JsonValue.Create(query[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var result = doc.Deserialize<T>(EntityConverter.GetJsonSerializerOptions());
|
||||||
|
return result switch {
|
||||||
|
null => OneFuzzResult<T>.Error(
|
||||||
|
ErrorCode.INVALID_REQUEST,
|
||||||
|
$"Failed to deserialize message into type: {typeof(T)} - {await req.ReadAsStringAsync()}"
|
||||||
|
),
|
||||||
|
var r => OneFuzzResult<T>.Ok(r),
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (JsonException exception) {
|
||||||
|
return OneFuzzResult<T>.Error(ConvertError(exception));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static Error ConvertError(Exception exception) {
|
public static Error ConvertError(Exception exception) {
|
||||||
return new Error(
|
return new Error(
|
||||||
ErrorCode.INVALID_REQUEST,
|
ErrorCode.INVALID_REQUEST,
|
||||||
|
@ -7,7 +7,8 @@ namespace Microsoft.OneFuzz.Service;
|
|||||||
|
|
||||||
public interface ISecretsOperations {
|
public interface ISecretsOperations {
|
||||||
public (Uri, string) ParseSecretUrl(Uri secretsUrl);
|
public (Uri, string) ParseSecretUrl(Uri secretsUrl);
|
||||||
public Task<SecretData<SecretAddress>?> SaveToKeyvault<T>(SecretData<T> secretData);
|
public Task<SecretAddress<T>> SaveToKeyvault<T>(SecretData<T> secretData);
|
||||||
|
|
||||||
public Task<string?> GetSecretStringValue<T>(SecretData<T> data);
|
public Task<string?> GetSecretStringValue<T>(SecretData<T> data);
|
||||||
|
|
||||||
public Task<KeyVaultSecret> StoreInKeyvault(Uri keyvaultUrl, string secretName, string secretValue);
|
public Task<KeyVaultSecret> StoreInKeyvault(Uri keyvaultUrl, string secretName, string secretValue);
|
||||||
@ -34,33 +35,30 @@ public class SecretsOperations : ISecretsOperations {
|
|||||||
return (new Uri(vaultUrl), secretName);
|
return (new Uri(vaultUrl), secretName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SecretData<SecretAddress>?> SaveToKeyvault<T>(SecretData<T> secretData) {
|
public async Task<SecretAddress<T>> SaveToKeyvault<T>(SecretData<T> secretData) {
|
||||||
if (secretData == null || secretData.Secret is null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (secretData.Secret is SecretAddress) {
|
if (secretData.Secret is SecretAddress<T> secretAddress) {
|
||||||
return secretData as SecretData<SecretAddress>;
|
return secretAddress;
|
||||||
} else {
|
} else if (secretData.Secret is SecretValue<T> sValue) {
|
||||||
var secretName = Guid.NewGuid();
|
var secretName = Guid.NewGuid();
|
||||||
string secretValue;
|
string secretValue;
|
||||||
if (secretData.Secret is string) {
|
if (sValue.Value is string secretString) {
|
||||||
secretValue = (secretData.Secret as string)!.Trim();
|
secretValue = secretString.Trim();
|
||||||
} else {
|
} else {
|
||||||
secretValue = JsonSerializer.Serialize(secretData.Secret, EntityConverter.GetJsonSerializerOptions());
|
secretValue = JsonSerializer.Serialize(sValue.Value, EntityConverter.GetJsonSerializerOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
var kv = await StoreInKeyvault(GetKeyvaultAddress(), secretName.ToString(), secretValue);
|
var kv = await StoreInKeyvault(GetKeyvaultAddress(), secretName.ToString(), secretValue);
|
||||||
return new SecretData<SecretAddress>(new SecretAddress(kv.Id));
|
return new SecretAddress<T>(kv.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new Exception("Invalid secret value");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string?> GetSecretStringValue<T>(SecretData<T> data) {
|
public async Task<string?> GetSecretStringValue<T>(SecretData<T> data) {
|
||||||
if (data.Secret is null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.Secret is SecretAddress) {
|
if (data.Secret is SecretAddress<T> secretAddress) {
|
||||||
var secret = await GetSecret((data.Secret as SecretAddress)!.Url);
|
var secret = await GetSecret(secretAddress.Url);
|
||||||
return secret.Value;
|
return secret.Value;
|
||||||
} else {
|
} else {
|
||||||
return data.Secret.ToString();
|
return data.Secret.ToString();
|
||||||
@ -101,11 +99,8 @@ public class SecretsOperations : ISecretsOperations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<DeleteSecretOperation?> DeleteRemoteSecretData<T>(SecretData<T> data) {
|
public async Task<DeleteSecretOperation?> DeleteRemoteSecretData<T>(SecretData<T> data) {
|
||||||
if (data.Secret is SecretAddress) {
|
if (data.Secret is SecretAddress<T> secretAddress) {
|
||||||
if (data.Secret is not null)
|
return await DeleteSecret(secretAddress.Url);
|
||||||
return await DeleteSecret((data.Secret as SecretAddress)!.Url);
|
|
||||||
else
|
|
||||||
return null;
|
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -110,7 +110,7 @@ public class TaskOperations : StatefulOrm<Task, TaskState, TaskOperations>, ITas
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Async.Task MarkDependantsFailed(Task task, List<Task>? taskInJob = null) {
|
private async Async.Task MarkDependantsFailed(Task task, List<Task>? taskInJob = null) {
|
||||||
taskInJob ??= await SearchByPartitionKey(task.JobId.ToString()).ToListAsync();
|
taskInJob ??= await SearchByPartitionKeys(new[] { task.JobId.ToString() }).ToListAsync();
|
||||||
|
|
||||||
foreach (var t in taskInJob) {
|
foreach (var t in taskInJob) {
|
||||||
if (t.Config.PrereqTasks != null) {
|
if (t.Config.PrereqTasks != null) {
|
||||||
|
@ -165,10 +165,10 @@ public sealed class PolymorphicConverterFactory : JsonConverterFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) {
|
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) {
|
||||||
var (field, attribute) = typeToConvert.GetProperties()
|
var (field, attribute) = typeToConvert
|
||||||
.Select(p => (p.Name, p.GetCustomAttribute<TypeDiscrimnatorAttribute>()))
|
.GetProperties()
|
||||||
.Where(p => p.Item2 != null)
|
.Select(p => (p.Name, p.GetCustomAttribute<TypeDiscrimnatorAttribute>()))
|
||||||
.First();
|
.First(p => p.Item2 != null);
|
||||||
|
|
||||||
|
|
||||||
return (JsonConverter)Activator.CreateInstance(
|
return (JsonConverter)Activator.CreateInstance(
|
||||||
|
@ -232,9 +232,8 @@ public class EntityConverter {
|
|||||||
return Guid.Parse(entity.GetString(ef.kind.ToString()));
|
return Guid.Parse(entity.GetString(ef.kind.ToString()));
|
||||||
else if (ef.type == typeof(int))
|
else if (ef.type == typeof(int))
|
||||||
return int.Parse(entity.GetString(ef.kind.ToString()));
|
return int.Parse(entity.GetString(ef.kind.ToString()));
|
||||||
else if (ef.type == typeof(PoolName))
|
else if (ef.type.IsClass)
|
||||||
// TODO: this should be able to be generic over any ValidatedString
|
return ef.type.GetConstructor(new[] { typeof(string) })!.Invoke(new[] { entity.GetString(ef.kind.ToString()) });
|
||||||
return PoolName.Parse(entity.GetString(ef.kind.ToString()));
|
|
||||||
else {
|
else {
|
||||||
throw new Exception($"invalid partition or row key type of {info.type} property {name}: {ef.type}");
|
throw new Exception($"invalid partition or row key type of {info.type} property {name}: {ef.type}");
|
||||||
}
|
}
|
||||||
|
@ -17,8 +17,8 @@ namespace ApiService.OneFuzzLib.Orm {
|
|||||||
Task<ResultVoid<(int, string)>> Delete(T entity);
|
Task<ResultVoid<(int, string)>> Delete(T entity);
|
||||||
|
|
||||||
IAsyncEnumerable<T> SearchAll();
|
IAsyncEnumerable<T> SearchAll();
|
||||||
IAsyncEnumerable<T> SearchByPartitionKey(string partitionKey);
|
IAsyncEnumerable<T> SearchByPartitionKeys(IEnumerable<string> partitionKeys);
|
||||||
IAsyncEnumerable<T> SearchByRowKey(string rowKey);
|
IAsyncEnumerable<T> SearchByRowKeys(IEnumerable<string> rowKeys);
|
||||||
IAsyncEnumerable<T> SearchByTimeRange(DateTimeOffset min, DateTimeOffset max);
|
IAsyncEnumerable<T> SearchByTimeRange(DateTimeOffset min, DateTimeOffset max);
|
||||||
|
|
||||||
// Allow using tuple to search.
|
// Allow using tuple to search.
|
||||||
@ -123,11 +123,11 @@ namespace ApiService.OneFuzzLib.Orm {
|
|||||||
public IAsyncEnumerable<T> SearchAll()
|
public IAsyncEnumerable<T> SearchAll()
|
||||||
=> QueryAsync(null);
|
=> QueryAsync(null);
|
||||||
|
|
||||||
public IAsyncEnumerable<T> SearchByPartitionKey(string partitionKey)
|
public IAsyncEnumerable<T> SearchByPartitionKeys(IEnumerable<string> partitionKeys)
|
||||||
=> QueryAsync(Query.PartitionKey(partitionKey));
|
=> QueryAsync(Query.PartitionKeys(partitionKeys));
|
||||||
|
|
||||||
public IAsyncEnumerable<T> SearchByRowKey(string rowKey)
|
public IAsyncEnumerable<T> SearchByRowKeys(IEnumerable<string> rowKeys)
|
||||||
=> QueryAsync(Query.RowKey(rowKey));
|
=> QueryAsync(Query.RowKeys(rowKeys));
|
||||||
|
|
||||||
public IAsyncEnumerable<T> SearchByTimeRange(DateTimeOffset min, DateTimeOffset max) {
|
public IAsyncEnumerable<T> SearchByTimeRange(DateTimeOffset min, DateTimeOffset max) {
|
||||||
return QueryAsync(Query.TimeRange(min, max));
|
return QueryAsync(Query.TimeRange(min, max));
|
||||||
|
@ -41,6 +41,18 @@ namespace Tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Gen<ISecret<T>> ISecret<T>() {
|
||||||
|
if (typeof(T) == typeof(string)) {
|
||||||
|
return Arb.Generate<string>().Select(s => (ISecret<T>)new SecretValue<string>(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof(T) == typeof(GithubAuth)) {
|
||||||
|
return Arb.Generate<GithubAuth>().Select(s => (ISecret<T>)new SecretValue<GithubAuth>(s));
|
||||||
|
} else {
|
||||||
|
throw new Exception($"Unsupported secret type {typeof(T)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static Gen<Version> Version() {
|
public static Gen<Version> Version() {
|
||||||
//OneFuzz version uses 3 number version
|
//OneFuzz version uses 3 number version
|
||||||
return Arb.Generate<Tuple<UInt16, UInt16, UInt16>>().Select(
|
return Arb.Generate<Tuple<UInt16, UInt16, UInt16>>().Select(
|
||||||
@ -270,7 +282,7 @@ namespace Tests {
|
|||||||
Id: arg.Item4,
|
Id: arg.Item4,
|
||||||
EventTime: arg.Item5
|
EventTime: arg.Item5
|
||||||
)
|
)
|
||||||
); ;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Gen<Report> Report() {
|
public static Gen<Report> Report() {
|
||||||
@ -341,6 +353,13 @@ namespace Tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Gen<NotificationTemplate> NotificationTemplate() {
|
||||||
|
return Gen.OneOf(new[] {
|
||||||
|
Arb.Generate<AdoTemplate>().Select(e => e as NotificationTemplate),
|
||||||
|
Arb.Generate<TeamsTemplate>().Select(e => e as NotificationTemplate),
|
||||||
|
Arb.Generate<GithubIssuesTemplate>().Select(e => e as NotificationTemplate)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public static Gen<Notification> Notification() {
|
public static Gen<Notification> Notification() {
|
||||||
return Arb.Generate<Tuple<Container, Guid, NotificationTemplate>>().Select(
|
return Arb.Generate<Tuple<Container, Guid, NotificationTemplate>>().Select(
|
||||||
@ -443,6 +462,11 @@ namespace Tests {
|
|||||||
return Arb.From(OrmGenerators.Container());
|
return Arb.From(OrmGenerators.Container());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static Arbitrary<NotificationTemplate> NotificationTemplate() {
|
||||||
|
return Arb.From(OrmGenerators.NotificationTemplate());
|
||||||
|
}
|
||||||
|
|
||||||
public static Arbitrary<Notification> Notification() {
|
public static Arbitrary<Notification> Notification() {
|
||||||
return Arb.From(OrmGenerators.Notification());
|
return Arb.From(OrmGenerators.Notification());
|
||||||
}
|
}
|
||||||
@ -454,6 +478,12 @@ namespace Tests {
|
|||||||
public static Arbitrary<Job> Job() {
|
public static Arbitrary<Job> Job() {
|
||||||
return Arb.From(OrmGenerators.Job());
|
return Arb.From(OrmGenerators.Job());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Arbitrary<ISecret<T>> ISecret<T>() {
|
||||||
|
return Arb.From(OrmGenerators.ISecret<T>());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user