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:
Cheick Keita
2023-04-19 14:27:16 -07:00
committed by GitHub
parent 6f06b8ffd4
commit aa28550aad
17 changed files with 244 additions and 103 deletions

View File

@ -238,6 +238,10 @@ If webhook is set to have Event Grid message format then the payload will look a
"title": "Onefuzz Version",
"type": "string"
},
"report_url": {
"title": "Report Url",
"type": "string"
},
"scariness_description": {
"title": "Scariness Description",
"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",
"type": "string"
},
"report_url": {
"title": "Report Url",
"type": "string"
},
"scariness_description": {
"title": "Scariness Description",
"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",
"type": "string"
},
"report_url": {
"title": "Report Url",
"type": "string"
},
"scariness_description": {
"title": "Scariness Description",
"type": "string"

View File

@ -22,7 +22,7 @@ public class Notifications {
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);

View 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"),
});
}
}

View File

@ -527,6 +527,10 @@ public record RegressionReport(
}
}
public record UnknownReportType(
Uri? ReportUrl
) : IReport;
[JsonConverter(typeof(NotificationTemplateConverter))]
#pragma warning disable CA1715
public interface NotificationTemplate {

View File

@ -129,6 +129,12 @@ public record NotificationSearch(
Guid? NotificationId
) : BaseRequest;
public record NotificationTest(
[property: Required] Report Report,
[property: Required] Notification Notification
) : BaseRequest;
public record NotificationGet(
[property: Required] Guid NotificationId
) : BaseRequest;

View File

@ -205,3 +205,8 @@ public record JinjaToScribanMigrationResponse(
public record JinjaToScribanMigrationDryRunResponse(
List<Guid> NotificationIdsToUpdate
) : BaseResponse();
public record NotificationTestResponse(
bool Success,
string? Error = null
) : BaseResponse();

View File

@ -10,6 +10,9 @@ public interface INotificationOperations : IOrm<Notification> {
IAsyncEnumerable<(Task, IEnumerable<Container>)> GetQueueTasks();
Async.Task<OneFuzzResult<Notification>> Create(Container container, NotificationTemplate config, bool replaceExisting);
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 {
@ -30,22 +33,7 @@ public class NotificationOperations : Orm<Notification>, INotificationOperations
}
done.Add(notification.Config);
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);
}
_ = await TriggerNotification(container, notification, reportOrRegression, isLastRetryAttempt);
}
}
@ -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) {
return SearchByRowKeys(new[] { container.String });
}

View File

@ -35,7 +35,17 @@ public class Reports : IReports {
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 (expectReports) {
@ -44,11 +54,9 @@ public class Reports : IReports {
return null;
}
var reportUrl = await _containers.GetFileUrl(container, fileName, StorageType.Corpus);
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");
}
@ -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);
if (regressionReport is { CrashTestResult: { } }) {
return regressionReport with { ReportUrl = reportUrl };
@ -73,12 +81,17 @@ public class Reports : IReports {
if (report is { CrashType: { } }) {
return report with { ReportUrl = reportUrl };
}
return null;
return new UnknownReportType(reportUrl);
}
}
public interface IReport {
Uri? ReportUrl {
init;
get;
}
public string FileName() {
var segments = (this.ReportUrl ?? throw new ArgumentException()).Segments.Skip(2);
return string.Concat(segments);
}
};

View File

@ -56,13 +56,11 @@ public class SecretsOperations : ISecretsOperations {
}
public async Task<string?> GetSecretStringValue<T>(SecretData<T> data) {
if (data.Secret is SecretAddress<T> secretAddress) {
var secret = await GetSecret(secretAddress.Url);
return secret.Value;
} else {
return data.Secret.ToString();
}
return (data.Secret) switch {
SecretAddress<T> secretAddress => (await GetSecret(secretAddress.Url)).Value,
SecretValue<T> sValue => sValue.Value?.ToString(),
_ => data.Secret.ToString(),
};
}
public Uri GetKeyvaultAddress() {

View File

@ -8,17 +8,18 @@ using Microsoft.VisualStudio.Services.WebApi.Patch.Json;
namespace Microsoft.OneFuzz.Service;
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 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) {
_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;
@ -44,8 +45,11 @@ public class Ado : NotificationsBase, IAdo {
} else {
_logTracer.WithTags(notificationInfo).Exception(e, $"Failed to process ado notification");
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) {
@ -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()) {
query += " where " + string.Join(" AND ", parts);
}
@ -331,47 +335,42 @@ public class Ado : NotificationsBase, IAdo {
}
public async Async.Task Process((string, string)[] notificationInfo) {
var matchingWorkItems = await ExistingWorkItems(notificationInfo).ToListAsync();
var nonDuplicateWorkItems = matchingWorkItems
.Where(wi => !IsADODuplicateWorkItem(wi))
.ToList();
if (nonDuplicateWorkItems.Count > 1) {
var nonDuplicateWorkItemIds = nonDuplicateWorkItems.Select(wi => wi.Id);
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);
var updated = false;
WorkItem? oldestWorkItem = null;
await foreach (var workItem in ExistingWorkItems(notificationInfo)) {
// work items are ordered by id, so the oldest one is the first one
oldestWorkItem ??= workItem;
_logTracer.WithTags(new List<(string, string)> { ("MatchingWorkItemIds", $"{workItem.Id}") }).Info($"Found matching work item");
if (IsADODuplicateWorkItem(workItem)) {
continue;
}
} else if (nonDuplicateWorkItems.Count == 1) {
_ = await UpdateExisting(nonDuplicateWorkItems.Single(), notificationInfo);
} else if (matchingWorkItems.Any()) {
// 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();
var stateChanged = await UpdateExisting(oldestWorkItem, notificationInfo);
if (stateChanged) {
// add a comment if we re-opened the bug
_ = await _client.AddCommentAsync(
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!);
_logTracer.WithTags(new List<(string, string)> { ("NonDuplicateWorkItemId", $"{workItem.Id}") }).Info($"Found matching non-duplicate work item");
_ = await UpdateExisting(workItem, notificationInfo);
updated = true;
}
if (!updated) {
if (oldestWorkItem != null) {
// 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 stateChanged = await UpdateExisting(oldestWorkItem, notificationInfo);
if (stateChanged) {
// add a comment if we re-opened the bug
_ = await _client.AddCommentAsync(
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}");
}
}

View File

@ -3,7 +3,7 @@
namespace Microsoft.OneFuzz.Service;
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 {
@ -11,10 +11,8 @@ public class GithubIssues : NotificationsBase, IGithubIssues {
public GithubIssues(ILogTracer logTracer, IOnefuzzContext context)
: base(logTracer, context) { }
public async Async.Task GithubIssue(GithubIssuesTemplate config, Container container, string filename, IReport? reportable, Guid notificationId) {
if (reportable == null) {
return;
}
public async Async.Task GithubIssue(GithubIssuesTemplate config, Container container, IReport reportable, Guid notificationId) {
var filename = reportable.FileName();
if (reportable is RegressionReport) {
_logTracer.Info($"github issue integration does not support regression reports. {container:Tag:Container} - {filename:Tag:Filename}");

View File

@ -4,7 +4,7 @@ using System.Text.Json;
namespace Microsoft.OneFuzz.Service;
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 {
@ -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>>();
string? text = null;
var title = string.Empty;
var filename = reportOrRegression.FileName();
if (reportOrRegression is Report report) {
var task = await _context.TaskOperations.GetByJobIdAndTaskId(report.JobId, report.TaskId);

View File

@ -84,7 +84,7 @@ public class ReportTests {
_ = Assert.IsType<RegressionReport>(regression);
var noReport = Reports.ParseReportOrRegression("{}", new Uri("http://test"));
Assert.Null(noReport);
_ = Assert.IsType<UnknownReportType>(noReport);

View File

@ -8,6 +8,7 @@ import logging
import os
import tempfile
import time
import uuid
from datetime import datetime
from typing import Any, Dict, List, Optional, Set, Tuple, Union
from urllib.parse import urlparse
@ -18,13 +19,13 @@ from azure.applicationinsights import ApplicationInsightsDataClient
from azure.applicationinsights.models import QueryBody
from azure.identity import AzureCliCredential
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.models import BlobRef, Job, NodeAssignment, Report, Task, TaskConfig
from onefuzztypes.primitives import Container, Directory, PoolName
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 .backend import wait
@ -775,6 +776,7 @@ class DebugNotification(Command):
"""Inject a report into the specified crash reporting task"""
task = self.onefuzz.tasks.get(task_id)
crashes = self._get_container(task, ContainerType.crashes)
reports = self._get_container(task, report_container_type)
@ -792,26 +794,15 @@ class DebugNotification(Command):
handle.write("")
self.onefuzz.containers.files.upload_file(crashes, file_path, crash_name)
report = Report(
input_blob=BlobRef(
account=self._get_storage_account(crashes),
container=crashes,
name=crash_name,
),
executable=task.config.task.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=task.job_id,
minimized_stack=[],
minimized_stack_function_names=[],
tool_name="libfuzzer",
tool_version="1.2.3",
onefuzz_version="1.2.3",
input_blob_ref = BlobRef(
account=self._get_storage_account(crashes),
container=crashes,
name=crash_name,
)
target_exe = task.config.task.target_exe if task.config.task.target_exe else ""
report = self._create_report(
task.job_id, task.task_id, target_exe, input_blob_ref
)
with tempfile.TemporaryDirectory() as tempdir:
@ -823,6 +814,58 @@ class DebugNotification(Command):
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):
"""Debug running jobs"""

View File

@ -231,6 +231,7 @@ class Report(BaseModel):
minimized_stack_function_names_sha256: Optional[str]
minimized_stack_function_lines: Optional[List[str]]
minimized_stack_function_lines_sha256: Optional[str]
report_url: Optional[str]
class NoReproReport(BaseModel):

View File

@ -8,6 +8,8 @@ from uuid import UUID
from pydantic import AnyHttpUrl, BaseModel, Field, root_validator
from onefuzztypes import models
from ._monkeypatch import _check_hotfix
from .consts import ONE_HOUR, SEVEN_DAYS
from .enums import (
@ -24,6 +26,7 @@ from .models import (
AutoScaleConfig,
InstanceConfig,
NotificationConfig,
Report,
TemplateRenderContext,
)
from .primitives import Container, PoolName, Region
@ -267,4 +270,9 @@ class JinjaToScribanMigrationPost(BaseModel):
dry_run: bool = Field(default=False)
class NotificationTest(BaseModel):
report: Report
notification: models.Notification
_check_hotfix()

View File

@ -99,3 +99,8 @@ class JinjaToScribanMigrationResponse(BaseResponse):
class JinjaToScribanMigrationDryRunResponse(BaseResponse):
notification_ids_to_update: List[UUID]
class NotificationTestResponse(BaseResponse):
success: bool
error: Optional[str]