mirror of
https://github.com/microsoft/onefuzz.git
synced 2025-06-17 12:28:07 +00:00
update the ado logic to consume the list of existing items once (#3014)
* update the ado logic to consume the list of existing items once * format * Update src/ApiService/ApiService/onefuzzlib/notifications/Ado.cs Co-authored-by: Teo Voinea <58236992+tevoinea@users.noreply.github.com> * Adding a notification testing endpoint * fix tests * format * regen docs * update logic * format * fix dummy name * mypy fix * make mypy happy * bandit fix * renaming * address PR Comment --------- Co-authored-by: Teo Voinea <58236992+tevoinea@users.noreply.github.com>
This commit is contained in:
@ -238,6 +238,10 @@ If webhook is set to have Event Grid message format then the payload will look a
|
|||||||
"title": "Onefuzz Version",
|
"title": "Onefuzz Version",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"report_url": {
|
||||||
|
"title": "Report Url",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"scariness_description": {
|
"scariness_description": {
|
||||||
"title": "Scariness Description",
|
"title": "Scariness Description",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -2158,6 +2162,10 @@ If webhook is set to have Event Grid message format then the payload will look a
|
|||||||
"title": "Onefuzz Version",
|
"title": "Onefuzz Version",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"report_url": {
|
||||||
|
"title": "Report Url",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"scariness_description": {
|
"scariness_description": {
|
||||||
"title": "Scariness Description",
|
"title": "Scariness Description",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -6578,6 +6586,10 @@ If webhook is set to have Event Grid message format then the payload will look a
|
|||||||
"title": "Onefuzz Version",
|
"title": "Onefuzz Version",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"report_url": {
|
||||||
|
"title": "Report Url",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"scariness_description": {
|
"scariness_description": {
|
||||||
"title": "Scariness Description",
|
"title": "Scariness Description",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
@ -22,7 +22,7 @@ public class Notifications {
|
|||||||
return await _context.RequestHandling.NotOk(req, request.ErrorV, "notification search");
|
return await _context.RequestHandling.NotOk(req, request.ErrorV, "notification search");
|
||||||
}
|
}
|
||||||
|
|
||||||
var entries = request.OkV switch { { Container: null, NotificationId: null } => _context.NotificationOperations.SearchAll(), { Container: var c, NotificationId: null } => _context.NotificationOperations.SearchByRowKeys(c.Select(x => x.String)), { Container: var _, NotificationId: var n } => new[] { await _context.NotificationOperations.GetNotification(n.Value) }.ToAsyncEnumerable(),
|
var entries = request.OkV switch { { Container: null, NotificationId: null } => _context.NotificationOperations.SearchAll(), { Container: var c, NotificationId: null } => _context.NotificationOperations.SearchByRowKeys(c.Select(x => x.String)), { Container: var _, NotificationId: var n } => new[] { await _context.NotificationOperations.GetNotification(n.Value) }.ToAsyncEnumerable()
|
||||||
};
|
};
|
||||||
|
|
||||||
var response = req.CreateResponse(HttpStatusCode.OK);
|
var response = req.CreateResponse(HttpStatusCode.OK);
|
||||||
|
41
src/ApiService/ApiService/Functions/NotificationsTest.cs
Normal file
41
src/ApiService/ApiService/Functions/NotificationsTest.cs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
using System.Net;
|
||||||
|
using Microsoft.Azure.Functions.Worker;
|
||||||
|
using Microsoft.Azure.Functions.Worker.Http;
|
||||||
|
|
||||||
|
namespace Microsoft.OneFuzz.Service.Functions;
|
||||||
|
|
||||||
|
public class NotificationsTest {
|
||||||
|
private readonly ILogTracer _log;
|
||||||
|
private readonly IEndpointAuthorization _auth;
|
||||||
|
private readonly IOnefuzzContext _context;
|
||||||
|
|
||||||
|
public NotificationsTest(ILogTracer log, IEndpointAuthorization auth, IOnefuzzContext context) {
|
||||||
|
_log = log;
|
||||||
|
_auth = auth;
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Async.Task<HttpResponseData> Post(HttpRequestData req) {
|
||||||
|
_log.WithTag("HttpRequest", "GET").Info($"Notification test");
|
||||||
|
var request = await RequestHandling.ParseRequest<NotificationTest>(req);
|
||||||
|
if (!request.IsOk) {
|
||||||
|
return await _context.RequestHandling.NotOk(req, request.ErrorV, "notification search");
|
||||||
|
}
|
||||||
|
|
||||||
|
var notificationTest = request.OkV;
|
||||||
|
var result = await _context.NotificationOperations.TriggerNotification(notificationTest.Notification.Container, notificationTest.Notification,
|
||||||
|
notificationTest.Report, isLastRetryAttempt: true);
|
||||||
|
var response = req.CreateResponse(HttpStatusCode.OK);
|
||||||
|
await response.WriteAsJsonAsync(new NotificationTestResponse(result.IsOk, result.ErrorV?.ToString()));
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Function("NotificationsTest")]
|
||||||
|
public Async.Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymous, "POST", Route = "notifications/test")] HttpRequestData req) {
|
||||||
|
return _auth.CallIfUser(req, r => r.Method switch {
|
||||||
|
"POST" => Post(r),
|
||||||
|
_ => throw new InvalidOperationException("Unsupported HTTP method"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -527,6 +527,10 @@ public record RegressionReport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record UnknownReportType(
|
||||||
|
Uri? ReportUrl
|
||||||
|
) : IReport;
|
||||||
|
|
||||||
[JsonConverter(typeof(NotificationTemplateConverter))]
|
[JsonConverter(typeof(NotificationTemplateConverter))]
|
||||||
#pragma warning disable CA1715
|
#pragma warning disable CA1715
|
||||||
public interface NotificationTemplate {
|
public interface NotificationTemplate {
|
||||||
|
@ -129,6 +129,12 @@ public record NotificationSearch(
|
|||||||
Guid? NotificationId
|
Guid? NotificationId
|
||||||
) : BaseRequest;
|
) : BaseRequest;
|
||||||
|
|
||||||
|
|
||||||
|
public record NotificationTest(
|
||||||
|
[property: Required] Report Report,
|
||||||
|
[property: Required] Notification Notification
|
||||||
|
) : BaseRequest;
|
||||||
|
|
||||||
public record NotificationGet(
|
public record NotificationGet(
|
||||||
[property: Required] Guid NotificationId
|
[property: Required] Guid NotificationId
|
||||||
) : BaseRequest;
|
) : BaseRequest;
|
||||||
|
@ -205,3 +205,8 @@ public record JinjaToScribanMigrationResponse(
|
|||||||
public record JinjaToScribanMigrationDryRunResponse(
|
public record JinjaToScribanMigrationDryRunResponse(
|
||||||
List<Guid> NotificationIdsToUpdate
|
List<Guid> NotificationIdsToUpdate
|
||||||
) : BaseResponse();
|
) : BaseResponse();
|
||||||
|
|
||||||
|
public record NotificationTestResponse(
|
||||||
|
bool Success,
|
||||||
|
string? Error = null
|
||||||
|
) : BaseResponse();
|
||||||
|
@ -10,6 +10,9 @@ public interface INotificationOperations : IOrm<Notification> {
|
|||||||
IAsyncEnumerable<(Task, IEnumerable<Container>)> GetQueueTasks();
|
IAsyncEnumerable<(Task, IEnumerable<Container>)> GetQueueTasks();
|
||||||
Async.Task<OneFuzzResult<Notification>> Create(Container container, NotificationTemplate config, bool replaceExisting);
|
Async.Task<OneFuzzResult<Notification>> Create(Container container, NotificationTemplate config, bool replaceExisting);
|
||||||
Async.Task<Notification?> GetNotification(Guid notifificationId);
|
Async.Task<Notification?> GetNotification(Guid notifificationId);
|
||||||
|
|
||||||
|
System.Threading.Tasks.Task<OneFuzzResultVoid> TriggerNotification(Container container,
|
||||||
|
Notification notification, IReport? reportOrRegression, bool isLastRetryAttempt = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NotificationOperations : Orm<Notification>, INotificationOperations {
|
public class NotificationOperations : Orm<Notification>, INotificationOperations {
|
||||||
@ -30,22 +33,7 @@ public class NotificationOperations : Orm<Notification>, INotificationOperations
|
|||||||
}
|
}
|
||||||
|
|
||||||
done.Add(notification.Config);
|
done.Add(notification.Config);
|
||||||
|
_ = await TriggerNotification(container, notification, reportOrRegression, isLastRetryAttempt);
|
||||||
if (notification.Config is TeamsTemplate teamsTemplate) {
|
|
||||||
await _context.Teams.NotifyTeams(teamsTemplate, container, filename, reportOrRegression!, notification.NotificationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reportOrRegression == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notification.Config is AdoTemplate adoTemplate) {
|
|
||||||
await _context.Ado.NotifyAdo(adoTemplate, container, filename, reportOrRegression, isLastRetryAttempt, notification.NotificationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notification.Config is GithubIssuesTemplate githubIssuesTemplate) {
|
|
||||||
await _context.GithubIssues.GithubIssue(githubIssuesTemplate, container, filename, reportOrRegression, notification.NotificationId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,6 +62,25 @@ public class NotificationOperations : Orm<Notification>, INotificationOperations
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async System.Threading.Tasks.Task<OneFuzzResultVoid> TriggerNotification(Container container,
|
||||||
|
Notification notification, IReport? reportOrRegression, bool isLastRetryAttempt = false) {
|
||||||
|
switch (notification.Config) {
|
||||||
|
case TeamsTemplate teamsTemplate:
|
||||||
|
await _context.Teams.NotifyTeams(teamsTemplate, container, reportOrRegression!,
|
||||||
|
notification.NotificationId);
|
||||||
|
break;
|
||||||
|
case AdoTemplate adoTemplate when reportOrRegression is not null:
|
||||||
|
return await _context.Ado.NotifyAdo(adoTemplate, container, reportOrRegression, isLastRetryAttempt,
|
||||||
|
notification.NotificationId);
|
||||||
|
case GithubIssuesTemplate githubIssuesTemplate when reportOrRegression is not null:
|
||||||
|
await _context.GithubIssues.GithubIssue(githubIssuesTemplate, container, reportOrRegression,
|
||||||
|
notification.NotificationId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return OneFuzzResultVoid.Ok;
|
||||||
|
}
|
||||||
|
|
||||||
public IAsyncEnumerable<Notification> GetNotifications(Container container) {
|
public IAsyncEnumerable<Notification> GetNotifications(Container container) {
|
||||||
return SearchByRowKeys(new[] { container.String });
|
return SearchByRowKeys(new[] { container.String });
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,17 @@ public class Reports : IReports {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var blob = await _containers.GetBlob(container, fileName, StorageType.Corpus);
|
var containerClient = await _containers.FindContainer(container, StorageType.Corpus);
|
||||||
|
if (containerClient == null) {
|
||||||
|
if (expectReports) {
|
||||||
|
_log.Error($"get_report invalid container: {filePath:Tag:FilePath}");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri reportUrl = containerClient.GetBlobClient(fileName).Uri;
|
||||||
|
|
||||||
|
var blob = (await containerClient.GetBlobClient(fileName).DownloadContentAsync()).Value.Content;
|
||||||
|
|
||||||
if (blob == null) {
|
if (blob == null) {
|
||||||
if (expectReports) {
|
if (expectReports) {
|
||||||
@ -44,11 +54,9 @@ public class Reports : IReports {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var reportUrl = await _containers.GetFileUrl(container, fileName, StorageType.Corpus);
|
|
||||||
|
|
||||||
var reportOrRegression = ParseReportOrRegression(blob.ToString(), reportUrl);
|
var reportOrRegression = ParseReportOrRegression(blob.ToString(), reportUrl);
|
||||||
|
|
||||||
if (reportOrRegression == null && expectReports) {
|
if (reportOrRegression is UnknownReportType && expectReports) {
|
||||||
_log.Error($"unable to parse report ({filePath:Tag:FilePath}) as a report or regression");
|
_log.Error($"unable to parse report ({filePath:Tag:FilePath}) as a report or regression");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +72,7 @@ public class Reports : IReports {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IReport? ParseReportOrRegression(string content, Uri? reportUrl) {
|
public static IReport ParseReportOrRegression(string content, Uri reportUrl) {
|
||||||
var regressionReport = TryDeserialize<RegressionReport>(content);
|
var regressionReport = TryDeserialize<RegressionReport>(content);
|
||||||
if (regressionReport is { CrashTestResult: { } }) {
|
if (regressionReport is { CrashTestResult: { } }) {
|
||||||
return regressionReport with { ReportUrl = reportUrl };
|
return regressionReport with { ReportUrl = reportUrl };
|
||||||
@ -73,12 +81,17 @@ public class Reports : IReports {
|
|||||||
if (report is { CrashType: { } }) {
|
if (report is { CrashType: { } }) {
|
||||||
return report with { ReportUrl = reportUrl };
|
return report with { ReportUrl = reportUrl };
|
||||||
}
|
}
|
||||||
return null;
|
return new UnknownReportType(reportUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IReport {
|
public interface IReport {
|
||||||
Uri? ReportUrl {
|
Uri? ReportUrl {
|
||||||
init;
|
init;
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
public string FileName() {
|
||||||
|
var segments = (this.ReportUrl ?? throw new ArgumentException()).Segments.Skip(2);
|
||||||
|
return string.Concat(segments);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -56,13 +56,11 @@ public class SecretsOperations : ISecretsOperations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string?> GetSecretStringValue<T>(SecretData<T> data) {
|
public async Task<string?> GetSecretStringValue<T>(SecretData<T> data) {
|
||||||
|
return (data.Secret) switch {
|
||||||
if (data.Secret is SecretAddress<T> secretAddress) {
|
SecretAddress<T> secretAddress => (await GetSecret(secretAddress.Url)).Value,
|
||||||
var secret = await GetSecret(secretAddress.Url);
|
SecretValue<T> sValue => sValue.Value?.ToString(),
|
||||||
return secret.Value;
|
_ => data.Secret.ToString(),
|
||||||
} else {
|
};
|
||||||
return data.Secret.ToString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Uri GetKeyvaultAddress() {
|
public Uri GetKeyvaultAddress() {
|
||||||
|
@ -8,17 +8,18 @@ using Microsoft.VisualStudio.Services.WebApi.Patch.Json;
|
|||||||
namespace Microsoft.OneFuzz.Service;
|
namespace Microsoft.OneFuzz.Service;
|
||||||
|
|
||||||
public interface IAdo {
|
public interface IAdo {
|
||||||
public Async.Task NotifyAdo(AdoTemplate config, Container container, string filename, IReport reportable, bool isLastRetryAttempt, Guid notificationId);
|
public Async.Task<OneFuzzResultVoid> NotifyAdo(AdoTemplate config, Container container, IReport reportable, bool isLastRetryAttempt, Guid notificationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Ado : NotificationsBase, IAdo {
|
public class Ado : NotificationsBase, IAdo {
|
||||||
public Ado(ILogTracer logTracer, IOnefuzzContext context) : base(logTracer, context) {
|
public Ado(ILogTracer logTracer, IOnefuzzContext context) : base(logTracer, context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Async.Task NotifyAdo(AdoTemplate config, Container container, string filename, IReport reportable, bool isLastRetryAttempt, Guid notificationId) {
|
public async Async.Task<OneFuzzResultVoid> NotifyAdo(AdoTemplate config, Container container, IReport reportable, bool isLastRetryAttempt, Guid notificationId) {
|
||||||
|
var filename = reportable.FileName();
|
||||||
if (reportable is RegressionReport) {
|
if (reportable is RegressionReport) {
|
||||||
_logTracer.Info($"ado integration does not support regression report. container:{container:Tag:Container} filename:{filename:Tag:Filename}");
|
_logTracer.Info($"ado integration does not support regression report. container:{container:Tag:Container} filename:{filename:Tag:Filename}");
|
||||||
return;
|
return OneFuzzResultVoid.Ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
var report = (Report)reportable;
|
var report = (Report)reportable;
|
||||||
@ -44,8 +45,11 @@ public class Ado : NotificationsBase, IAdo {
|
|||||||
} else {
|
} else {
|
||||||
_logTracer.WithTags(notificationInfo).Exception(e, $"Failed to process ado notification");
|
_logTracer.WithTags(notificationInfo).Exception(e, $"Failed to process ado notification");
|
||||||
await LogFailedNotification(report, e, notificationId);
|
await LogFailedNotification(report, e, notificationId);
|
||||||
|
return OneFuzzResultVoid.Error(ErrorCode.NOTIFICATION_FAILURE,
|
||||||
|
$"Failed to process ado notification : exception: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return OneFuzzResultVoid.Ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsTransient(Exception e) {
|
private static bool IsTransient(Exception e) {
|
||||||
@ -205,7 +209,7 @@ public class Ado : NotificationsBase, IAdo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var query = "select [System.Id] from WorkItems";
|
var query = "select [System.Id] from WorkItems order by [System.Id]";
|
||||||
if (parts != null && parts.Any()) {
|
if (parts != null && parts.Any()) {
|
||||||
query += " where " + string.Join(" AND ", parts);
|
query += " where " + string.Join(" AND ", parts);
|
||||||
}
|
}
|
||||||
@ -331,47 +335,42 @@ public class Ado : NotificationsBase, IAdo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Async.Task Process((string, string)[] notificationInfo) {
|
public async Async.Task Process((string, string)[] notificationInfo) {
|
||||||
var matchingWorkItems = await ExistingWorkItems(notificationInfo).ToListAsync();
|
var updated = false;
|
||||||
|
WorkItem? oldestWorkItem = null;
|
||||||
var nonDuplicateWorkItems = matchingWorkItems
|
await foreach (var workItem in ExistingWorkItems(notificationInfo)) {
|
||||||
.Where(wi => !IsADODuplicateWorkItem(wi))
|
// work items are ordered by id, so the oldest one is the first one
|
||||||
.ToList();
|
oldestWorkItem ??= workItem;
|
||||||
|
_logTracer.WithTags(new List<(string, string)> { ("MatchingWorkItemIds", $"{workItem.Id}") }).Info($"Found matching work item");
|
||||||
if (nonDuplicateWorkItems.Count > 1) {
|
if (IsADODuplicateWorkItem(workItem)) {
|
||||||
var nonDuplicateWorkItemIds = nonDuplicateWorkItems.Select(wi => wi.Id);
|
continue;
|
||||||
var matchingWorkItemIds = matchingWorkItems.Select(wi => wi.Id);
|
|
||||||
|
|
||||||
var extraTags = new List<(string, string)> {
|
|
||||||
("NonDuplicateWorkItemIds", JsonSerializer.Serialize(nonDuplicateWorkItemIds)),
|
|
||||||
("MatchingWorkItemIds", JsonSerializer.Serialize(matchingWorkItemIds))
|
|
||||||
};
|
|
||||||
extraTags.AddRange(notificationInfo);
|
|
||||||
|
|
||||||
_logTracer.WithTags(extraTags).Info($"Found more than 1 matching, non-duplicate work item");
|
|
||||||
foreach (var workItem in nonDuplicateWorkItems) {
|
|
||||||
_ = await UpdateExisting(workItem, notificationInfo);
|
|
||||||
}
|
}
|
||||||
} else if (nonDuplicateWorkItems.Count == 1) {
|
_logTracer.WithTags(new List<(string, string)> { ("NonDuplicateWorkItemId", $"{workItem.Id}") }).Info($"Found matching non-duplicate work item");
|
||||||
_ = await UpdateExisting(nonDuplicateWorkItems.Single(), notificationInfo);
|
_ = await UpdateExisting(workItem, notificationInfo);
|
||||||
} else if (matchingWorkItems.Any()) {
|
updated = true;
|
||||||
// We have matching work items but all are duplicates
|
}
|
||||||
_logTracer.WithTags(notificationInfo).Info($"All matching work items were duplicates, re-opening the oldest one");
|
|
||||||
var oldestWorkItem = matchingWorkItems.OrderBy(wi => wi.Id).First();
|
if (!updated) {
|
||||||
var stateChanged = await UpdateExisting(oldestWorkItem, notificationInfo);
|
if (oldestWorkItem != null) {
|
||||||
if (stateChanged) {
|
// We have matching work items but all are duplicates
|
||||||
// add a comment if we re-opened the bug
|
_logTracer.WithTags(notificationInfo)
|
||||||
_ = await _client.AddCommentAsync(
|
.Info($"All matching work items were duplicates, re-opening the oldest one");
|
||||||
new CommentCreate() {
|
var stateChanged = await UpdateExisting(oldestWorkItem, notificationInfo);
|
||||||
Text = "This work item was re-opened because OneFuzz could only find related work items that are marked as duplicate."
|
if (stateChanged) {
|
||||||
},
|
// add a comment if we re-opened the bug
|
||||||
_project,
|
_ = await _client.AddCommentAsync(
|
||||||
(int)oldestWorkItem.Id!);
|
new CommentCreate() {
|
||||||
|
Text =
|
||||||
|
"This work item was re-opened because OneFuzz could only find related work items that are marked as duplicate."
|
||||||
|
},
|
||||||
|
_project,
|
||||||
|
(int)oldestWorkItem.Id!);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We never saw a work item like this before, it must be new
|
||||||
|
var entry = await CreateNew();
|
||||||
|
var adoEventType = "AdoNewItem";
|
||||||
|
_logTracer.WithTags(notificationInfo).Event($"{adoEventType} {entry.Id:Tag:WorkItemId}");
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// We never saw a work item like this before, it must be new
|
|
||||||
var entry = await CreateNew();
|
|
||||||
var adoEventType = "AdoNewItem";
|
|
||||||
_logTracer.WithTags(notificationInfo).Event($"{adoEventType} {entry.Id:Tag:WorkItemId}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
namespace Microsoft.OneFuzz.Service;
|
namespace Microsoft.OneFuzz.Service;
|
||||||
|
|
||||||
public interface IGithubIssues {
|
public interface IGithubIssues {
|
||||||
Async.Task GithubIssue(GithubIssuesTemplate config, Container container, string filename, IReport? reportable, Guid notificationId);
|
Async.Task GithubIssue(GithubIssuesTemplate config, Container container, IReport reportable, Guid notificationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GithubIssues : NotificationsBase, IGithubIssues {
|
public class GithubIssues : NotificationsBase, IGithubIssues {
|
||||||
@ -11,10 +11,8 @@ public class GithubIssues : NotificationsBase, IGithubIssues {
|
|||||||
public GithubIssues(ILogTracer logTracer, IOnefuzzContext context)
|
public GithubIssues(ILogTracer logTracer, IOnefuzzContext context)
|
||||||
: base(logTracer, context) { }
|
: base(logTracer, context) { }
|
||||||
|
|
||||||
public async Async.Task GithubIssue(GithubIssuesTemplate config, Container container, string filename, IReport? reportable, Guid notificationId) {
|
public async Async.Task GithubIssue(GithubIssuesTemplate config, Container container, IReport reportable, Guid notificationId) {
|
||||||
if (reportable == null) {
|
var filename = reportable.FileName();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reportable is RegressionReport) {
|
if (reportable is RegressionReport) {
|
||||||
_logTracer.Info($"github issue integration does not support regression reports. {container:Tag:Container} - {filename:Tag:Filename}");
|
_logTracer.Info($"github issue integration does not support regression reports. {container:Tag:Container} - {filename:Tag:Filename}");
|
||||||
|
@ -4,7 +4,7 @@ using System.Text.Json;
|
|||||||
namespace Microsoft.OneFuzz.Service;
|
namespace Microsoft.OneFuzz.Service;
|
||||||
|
|
||||||
public interface ITeams {
|
public interface ITeams {
|
||||||
Async.Task NotifyTeams(TeamsTemplate config, Container container, string filename, IReport reportOrRegression, Guid notificationId);
|
Async.Task NotifyTeams(TeamsTemplate config, Container container, IReport reportOrRegression, Guid notificationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Teams : ITeams {
|
public class Teams : ITeams {
|
||||||
@ -53,10 +53,11 @@ public class Teams : ITeams {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Async.Task NotifyTeams(TeamsTemplate config, Container container, string filename, IReport reportOrRegression, Guid notificationId) {
|
public async Async.Task NotifyTeams(TeamsTemplate config, Container container, IReport reportOrRegression, Guid notificationId) {
|
||||||
var facts = new List<Dictionary<string, string>>();
|
var facts = new List<Dictionary<string, string>>();
|
||||||
string? text = null;
|
string? text = null;
|
||||||
var title = string.Empty;
|
var title = string.Empty;
|
||||||
|
var filename = reportOrRegression.FileName();
|
||||||
|
|
||||||
if (reportOrRegression is Report report) {
|
if (reportOrRegression is Report report) {
|
||||||
var task = await _context.TaskOperations.GetByJobIdAndTaskId(report.JobId, report.TaskId);
|
var task = await _context.TaskOperations.GetByJobIdAndTaskId(report.JobId, report.TaskId);
|
||||||
|
@ -84,7 +84,7 @@ public class ReportTests {
|
|||||||
_ = Assert.IsType<RegressionReport>(regression);
|
_ = Assert.IsType<RegressionReport>(regression);
|
||||||
|
|
||||||
var noReport = Reports.ParseReportOrRegression("{}", new Uri("http://test"));
|
var noReport = Reports.ParseReportOrRegression("{}", new Uri("http://test"));
|
||||||
Assert.Null(noReport);
|
_ = Assert.IsType<UnknownReportType>(noReport);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
@ -18,13 +19,13 @@ from azure.applicationinsights import ApplicationInsightsDataClient
|
|||||||
from azure.applicationinsights.models import QueryBody
|
from azure.applicationinsights.models import QueryBody
|
||||||
from azure.identity import AzureCliCredential
|
from azure.identity import AzureCliCredential
|
||||||
from azure.storage.blob import ContainerClient
|
from azure.storage.blob import ContainerClient
|
||||||
from onefuzztypes import models, requests
|
from onefuzztypes import models, requests, responses
|
||||||
from onefuzztypes.enums import ContainerType, TaskType
|
from onefuzztypes.enums import ContainerType, TaskType
|
||||||
from onefuzztypes.models import BlobRef, Job, NodeAssignment, Report, Task, TaskConfig
|
from onefuzztypes.models import BlobRef, Job, NodeAssignment, Report, Task, TaskConfig
|
||||||
from onefuzztypes.primitives import Container, Directory, PoolName
|
from onefuzztypes.primitives import Container, Directory, PoolName
|
||||||
from onefuzztypes.responses import TemplateValidationResponse
|
from onefuzztypes.responses import TemplateValidationResponse
|
||||||
|
|
||||||
from onefuzz.api import UUID_EXPANSION, Command, Onefuzz
|
from onefuzz.api import UUID_EXPANSION, Command, Endpoint, Onefuzz
|
||||||
|
|
||||||
from .azure_identity_credential_adapter import AzureIdentityCredentialAdapter
|
from .azure_identity_credential_adapter import AzureIdentityCredentialAdapter
|
||||||
from .backend import wait
|
from .backend import wait
|
||||||
@ -775,6 +776,7 @@ class DebugNotification(Command):
|
|||||||
"""Inject a report into the specified crash reporting task"""
|
"""Inject a report into the specified crash reporting task"""
|
||||||
|
|
||||||
task = self.onefuzz.tasks.get(task_id)
|
task = self.onefuzz.tasks.get(task_id)
|
||||||
|
|
||||||
crashes = self._get_container(task, ContainerType.crashes)
|
crashes = self._get_container(task, ContainerType.crashes)
|
||||||
reports = self._get_container(task, report_container_type)
|
reports = self._get_container(task, report_container_type)
|
||||||
|
|
||||||
@ -792,26 +794,15 @@ class DebugNotification(Command):
|
|||||||
handle.write("")
|
handle.write("")
|
||||||
self.onefuzz.containers.files.upload_file(crashes, file_path, crash_name)
|
self.onefuzz.containers.files.upload_file(crashes, file_path, crash_name)
|
||||||
|
|
||||||
report = Report(
|
input_blob_ref = BlobRef(
|
||||||
input_blob=BlobRef(
|
account=self._get_storage_account(crashes),
|
||||||
account=self._get_storage_account(crashes),
|
container=crashes,
|
||||||
container=crashes,
|
name=crash_name,
|
||||||
name=crash_name,
|
)
|
||||||
),
|
|
||||||
executable=task.config.task.target_exe,
|
target_exe = task.config.task.target_exe if task.config.task.target_exe else ""
|
||||||
crash_type="fake crash report",
|
report = self._create_report(
|
||||||
crash_site="fake crash site",
|
task.job_id, task.task_id, target_exe, input_blob_ref
|
||||||
call_stack=["#0 fake", "#1 call", "#2 stack"],
|
|
||||||
call_stack_sha256=ZERO_SHA256,
|
|
||||||
input_sha256=EMPTY_SHA256,
|
|
||||||
asan_log="fake asan log",
|
|
||||||
task_id=task_id,
|
|
||||||
job_id=task.job_id,
|
|
||||||
minimized_stack=[],
|
|
||||||
minimized_stack_function_names=[],
|
|
||||||
tool_name="libfuzzer",
|
|
||||||
tool_version="1.2.3",
|
|
||||||
onefuzz_version="1.2.3",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tempdir:
|
with tempfile.TemporaryDirectory() as tempdir:
|
||||||
@ -823,6 +814,58 @@ class DebugNotification(Command):
|
|||||||
reports, file_path, crash_name + ".json"
|
reports, file_path, crash_name + ".json"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_template(
|
||||||
|
self, task_id: UUID_EXPANSION, notificationConfig: models.NotificationConfig
|
||||||
|
) -> responses.NotificationTestResponse:
|
||||||
|
"""Test a notification template"""
|
||||||
|
endpoint = Endpoint(self.onefuzz)
|
||||||
|
task = self.onefuzz.tasks.get(task_id)
|
||||||
|
input_blob_ref = BlobRef(
|
||||||
|
account="dummy-storage-account",
|
||||||
|
container="test-notification-crashes",
|
||||||
|
name="fake-crash-sample",
|
||||||
|
)
|
||||||
|
|
||||||
|
report = self._create_report(
|
||||||
|
task.job_id, task.task_id, "fake_target.exe", input_blob_ref
|
||||||
|
)
|
||||||
|
report.report_url = "https://dummy-container.blob.core.windows.net/dummy-reports/dummy-report.json"
|
||||||
|
|
||||||
|
return endpoint._req_model(
|
||||||
|
"POST",
|
||||||
|
responses.NotificationTestResponse,
|
||||||
|
data=requests.NotificationTest(
|
||||||
|
report=report,
|
||||||
|
notification=models.Notification(
|
||||||
|
container=Container("test-notification-reports"),
|
||||||
|
notification_id=uuid.uuid4(),
|
||||||
|
config=notificationConfig.config,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
alternate_endpoint="notifications/test",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_report(
|
||||||
|
self, job_id: UUID, task_id: UUID, target_exe: str, input_blob_ref: BlobRef
|
||||||
|
) -> Report:
|
||||||
|
return Report(
|
||||||
|
input_blob=input_blob_ref,
|
||||||
|
executable=target_exe,
|
||||||
|
crash_type="fake crash report",
|
||||||
|
crash_site="fake crash site",
|
||||||
|
call_stack=["#0 fake", "#1 call", "#2 stack"],
|
||||||
|
call_stack_sha256=ZERO_SHA256,
|
||||||
|
input_sha256=EMPTY_SHA256,
|
||||||
|
asan_log="fake asan log",
|
||||||
|
task_id=task_id,
|
||||||
|
job_id=job_id,
|
||||||
|
minimized_stack=[],
|
||||||
|
minimized_stack_function_names=[],
|
||||||
|
tool_name="libfuzzer",
|
||||||
|
tool_version="1.2.3",
|
||||||
|
onefuzz_version="1.2.3",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Debug(Command):
|
class Debug(Command):
|
||||||
"""Debug running jobs"""
|
"""Debug running jobs"""
|
||||||
|
@ -231,6 +231,7 @@ class Report(BaseModel):
|
|||||||
minimized_stack_function_names_sha256: Optional[str]
|
minimized_stack_function_names_sha256: Optional[str]
|
||||||
minimized_stack_function_lines: Optional[List[str]]
|
minimized_stack_function_lines: Optional[List[str]]
|
||||||
minimized_stack_function_lines_sha256: Optional[str]
|
minimized_stack_function_lines_sha256: Optional[str]
|
||||||
|
report_url: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
class NoReproReport(BaseModel):
|
class NoReproReport(BaseModel):
|
||||||
|
@ -8,6 +8,8 @@ from uuid import UUID
|
|||||||
|
|
||||||
from pydantic import AnyHttpUrl, BaseModel, Field, root_validator
|
from pydantic import AnyHttpUrl, BaseModel, Field, root_validator
|
||||||
|
|
||||||
|
from onefuzztypes import models
|
||||||
|
|
||||||
from ._monkeypatch import _check_hotfix
|
from ._monkeypatch import _check_hotfix
|
||||||
from .consts import ONE_HOUR, SEVEN_DAYS
|
from .consts import ONE_HOUR, SEVEN_DAYS
|
||||||
from .enums import (
|
from .enums import (
|
||||||
@ -24,6 +26,7 @@ from .models import (
|
|||||||
AutoScaleConfig,
|
AutoScaleConfig,
|
||||||
InstanceConfig,
|
InstanceConfig,
|
||||||
NotificationConfig,
|
NotificationConfig,
|
||||||
|
Report,
|
||||||
TemplateRenderContext,
|
TemplateRenderContext,
|
||||||
)
|
)
|
||||||
from .primitives import Container, PoolName, Region
|
from .primitives import Container, PoolName, Region
|
||||||
@ -267,4 +270,9 @@ class JinjaToScribanMigrationPost(BaseModel):
|
|||||||
dry_run: bool = Field(default=False)
|
dry_run: bool = Field(default=False)
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationTest(BaseModel):
|
||||||
|
report: Report
|
||||||
|
notification: models.Notification
|
||||||
|
|
||||||
|
|
||||||
_check_hotfix()
|
_check_hotfix()
|
||||||
|
@ -99,3 +99,8 @@ class JinjaToScribanMigrationResponse(BaseResponse):
|
|||||||
|
|
||||||
class JinjaToScribanMigrationDryRunResponse(BaseResponse):
|
class JinjaToScribanMigrationDryRunResponse(BaseResponse):
|
||||||
notification_ids_to_update: List[UUID]
|
notification_ids_to_update: List[UUID]
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationTestResponse(BaseResponse):
|
||||||
|
success: bool
|
||||||
|
error: Optional[str]
|
||||||
|
Reference in New Issue
Block a user