Unify Dashboard & Webhook events (#394)

This change unifies the previously adhoc SignalR events and Webhooks into a single event format.
This commit is contained in:
bmc-msft
2021-01-11 16:43:09 -05:00
committed by GitHub
parent 465727680d
commit 513d1f52c9
37 changed files with 2970 additions and 825 deletions

View File

@ -5,17 +5,55 @@
from typing import Optional
from uuid import UUID
import json
from onefuzztypes.enums import TaskType, ContainerType, ErrorCode
from onefuzztypes.models import TaskConfig, TaskDetails, TaskContainers, Error, UserInfo
from onefuzztypes.webhooks import (
WebhookMessage,
WebhookEventPing,
WebhookEventTaskCreated,
WebhookEventTaskStopped,
WebhookEventTaskFailed,
from onefuzztypes.primitives import Region, Container
from onefuzztypes.enums import (
TaskType,
ContainerType,
ErrorCode,
OS,
Architecture,
NodeState,
)
from onefuzztypes.enums import WebhookEventType
from onefuzztypes.models import (
TaskConfig,
TaskDetails,
TaskContainers,
TaskState,
Error,
UserInfo,
JobConfig,
Report,
BlobRef,
)
from onefuzztypes.events import (
Event,
EventPing,
EventCrashReported,
EventFileAdded,
EventTaskCreated,
EventTaskStopped,
EventTaskFailed,
EventProxyCreated,
EventProxyDeleted,
EventProxyFailed,
EventPoolCreated,
EventPoolDeleted,
EventScalesetCreated,
EventScalesetFailed,
EventScalesetDeleted,
EventJobCreated,
EventJobStopped,
EventTaskStateUpdated,
EventNodeStateUpdated,
EventNodeCreated,
EventNodeDeleted,
get_event_type,
EventType,
)
from onefuzztypes.webhooks import WebhookMessage
EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
ZERO_SHA256 = "0" * len(EMPTY_SHA256)
def layer(depth: int, title: str, content: Optional[str] = None) -> None:
@ -29,28 +67,9 @@ def typed(depth: int, title: str, content: str, data_type: str) -> None:
def main():
examples = {
WebhookEventType.ping: WebhookEventPing(ping_id=UUID(int=0)),
WebhookEventType.task_stopped: WebhookEventTaskStopped(
job_id=UUID(int=0),
task_id=UUID(int=0),
user_info=UserInfo(
application_id=UUID(int=0),
object_id=UUID(int=0),
upn="example@contoso.com",
),
),
WebhookEventType.task_failed: WebhookEventTaskFailed(
job_id=UUID(int=0),
task_id=UUID(int=0),
error=Error(code=ErrorCode.TASK_FAILED, errors=["example error message"]),
user_info=UserInfo(
application_id=UUID(int=0),
object_id=UUID(int=0),
upn="example@contoso.com",
),
),
WebhookEventType.task_created: WebhookEventTaskCreated(
examples = [
EventPing(ping_id=UUID(int=0)),
EventTaskCreated(
job_id=UUID(int=0),
task_id=UUID(int=0),
config=TaskConfig(
@ -75,13 +94,122 @@ def main():
upn="example@contoso.com",
),
),
}
EventTaskStopped(
job_id=UUID(int=0),
task_id=UUID(int=0),
user_info=UserInfo(
application_id=UUID(int=0),
object_id=UUID(int=0),
upn="example@contoso.com",
),
),
EventTaskFailed(
job_id=UUID(int=0),
task_id=UUID(int=0),
error=Error(code=ErrorCode.TASK_FAILED, errors=["example error message"]),
user_info=UserInfo(
application_id=UUID(int=0),
object_id=UUID(int=0),
upn="example@contoso.com",
),
),
EventTaskStateUpdated(
job_id=UUID(int=0), task_id=UUID(int=0), state=TaskState.init
),
EventProxyCreated(region=Region("eastus")),
EventProxyDeleted(region=Region("eastus")),
EventProxyFailed(
region=Region("eastus"),
error=Error(code=ErrorCode.PROXY_FAILED, errors=["example error message"]),
),
EventPoolCreated(
pool_name="example", os=OS.linux, arch=Architecture.x86_64, managed=True
),
EventPoolDeleted(pool_name="example"),
EventScalesetCreated(
scaleset_id=UUID(int=0),
pool_name="example",
vm_sku="Standard_D2s_v3",
image="Canonical:UbuntuServer:18.04-LTS:latest",
region=Region("eastus"),
size=10,
),
EventScalesetFailed(
scaleset_id=UUID(int=0),
pool_name="example",
error=Error(
code=ErrorCode.UNABLE_TO_RESIZE, errors=["example error message"]
),
),
EventScalesetDeleted(scaleset_id=UUID(int=0), pool_name="example"),
EventJobCreated(
job_id=UUID(int=0),
config=JobConfig(
project="example project",
name="example name",
build="build 1",
duration=24,
),
),
EventJobStopped(
job_id=UUID(int=0),
config=JobConfig(
project="example project",
name="example name",
build="build 1",
duration=24,
),
),
EventNodeCreated(machine_id=UUID(int=0), pool_name="example"),
EventNodeDeleted(machine_id=UUID(int=0), pool_name="example"),
EventNodeStateUpdated(
machine_id=UUID(int=0), pool_name="example", state=NodeState.setting_up
),
EventCrashReported(
container=Container("container-name"),
filename="example.json",
report=Report(
input_blob=BlobRef(
account="contoso-storage-account",
container=Container("crashes"),
name="input.txt",
),
executable="fuzz.exe",
crash_type="example crash report type",
crash_site="example crash site",
call_stack=["#0 line", "#1 line", "#2 line"],
call_stack_sha256=ZERO_SHA256,
input_sha256=EMPTY_SHA256,
asan_log="example asan log",
task_id=UUID(int=0),
job_id=UUID(int=0),
scariness_score=10,
scariness_description="example-scariness",
),
),
EventFileAdded(container=Container("container-name"), filename="example.txt"),
]
for event in Event.__args__:
seen = False
for value in examples:
if isinstance(value, event):
seen = True
break
assert seen, "missing event type definition: %s" % event.__name__
event_types = [get_event_type(x) for x in examples]
for event_type in EventType:
assert event_type in event_types, (
"missing event type definition: %s" % event_type.name
)
message = WebhookMessage(
webhook_id=UUID(int=0),
event_id=UUID(int=0),
event_type=WebhookEventType.ping,
event=examples[WebhookEventType.ping],
event_type=EventType.ping,
event=EventPing(ping_id=UUID(int=0)),
)
layer(
@ -96,11 +224,18 @@ def main():
)
typed(3, "Example", message.json(indent=4, exclude_none=True), "json")
layer(2, "Event Types (WebhookEventType)")
layer(2, "Event Types (EventType)")
for webhook_type in WebhookEventType:
example = examples[webhook_type]
layer(3, webhook_type.name)
event_map = {get_event_type(x).name: x for x in examples}
for name in sorted(event_map.keys()):
print(f"* [{name}](#{name})")
print()
for name in sorted(event_map.keys()):
example = event_map[name]
layer(3, name)
typed(4, "Example", example.json(indent=4, exclude_none=True), "json")
typed(4, "Schema", example.schema_json(indent=4), "json")

View File

@ -252,6 +252,7 @@ class ErrorCode(Enum):
INVALID_NODE = 469
NOTIFICATION_FAILURE = 470
UNABLE_TO_UPDATE = 471
PROXY_FAILED = 472
class HeartbeatType(Enum):
@ -368,13 +369,6 @@ class TaskDebugFlag(Enum):
keep_node_on_completion = "keep_node_on_completion"
class WebhookEventType(Enum):
task_created = "task_created"
task_stopped = "task_stopped"
task_failed = "task_failed"
ping = "ping"
class WebhookMessageState(Enum):
queued = "queued"
retrying = "retrying"

View File

@ -0,0 +1,225 @@
#!/usr/bin/env python
#
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
from datetime import datetime
from enum import Enum
from typing import Optional, Union
from uuid import UUID, uuid4
from pydantic import BaseModel, Extra, Field
from .enums import OS, Architecture, NodeState, TaskState
from .models import AutoScaleConfig, Error, JobConfig, Report, TaskConfig, UserInfo
from .primitives import Container, Region
from .responses import BaseResponse
class BaseEvent(BaseModel):
class Config:
extra = Extra.forbid
class EventTaskStopped(BaseEvent):
job_id: UUID
task_id: UUID
user_info: Optional[UserInfo]
class EventTaskFailed(BaseEvent):
job_id: UUID
task_id: UUID
error: Error
user_info: Optional[UserInfo]
class EventJobCreated(BaseEvent):
job_id: UUID
config: JobConfig
user_info: Optional[UserInfo]
class EventJobStopped(BaseEvent):
job_id: UUID
config: JobConfig
user_info: Optional[UserInfo]
class EventTaskCreated(BaseEvent):
job_id: UUID
task_id: UUID
config: TaskConfig
user_info: Optional[UserInfo]
class EventTaskStateUpdated(BaseEvent):
job_id: UUID
task_id: UUID
state: TaskState
end_time: Optional[datetime]
class EventPing(BaseResponse):
ping_id: UUID
class EventScalesetCreated(BaseEvent):
scaleset_id: UUID
pool_name: str
vm_sku: str
image: str
region: Region
size: int
class EventScalesetFailed(BaseEvent):
scaleset_id: UUID
pool_name: str
error: Error
class EventScalesetDeleted(BaseEvent):
scaleset_id: UUID
pool_name: str
class EventPoolDeleted(BaseEvent):
pool_name: str
class EventPoolCreated(BaseEvent):
pool_name: str
os: OS
arch: Architecture
managed: bool
autoscale: Optional[AutoScaleConfig]
class EventProxyCreated(BaseEvent):
region: Region
class EventProxyDeleted(BaseEvent):
region: Region
class EventProxyFailed(BaseEvent):
region: Region
error: Error
class EventNodeCreated(BaseEvent):
machine_id: UUID
scaleset_id: Optional[UUID]
pool_name: str
class EventNodeDeleted(BaseEvent):
machine_id: UUID
scaleset_id: Optional[UUID]
pool_name: str
class EventNodeStateUpdated(BaseEvent):
machine_id: UUID
scaleset_id: Optional[UUID]
pool_name: str
state: NodeState
class EventCrashReported(BaseEvent):
report: Report
container: Container
filename: str
class EventFileAdded(BaseEvent):
container: Container
filename: str
Event = Union[
EventJobCreated,
EventJobStopped,
EventNodeStateUpdated,
EventNodeCreated,
EventNodeDeleted,
EventPing,
EventPoolCreated,
EventPoolDeleted,
EventProxyFailed,
EventProxyCreated,
EventProxyDeleted,
EventScalesetFailed,
EventScalesetCreated,
EventScalesetDeleted,
EventTaskFailed,
EventTaskStateUpdated,
EventTaskCreated,
EventTaskStopped,
EventCrashReported,
EventFileAdded,
]
class EventType(Enum):
job_created = "job_created"
job_stopped = "job_stopped"
node_created = "node_created"
node_deleted = "node_deleted"
node_state_updated = "node_state_updated"
ping = "ping"
pool_created = "pool_created"
pool_deleted = "pool_deleted"
proxy_created = "proxy_created"
proxy_deleted = "proxy_deleted"
proxy_failed = "proxy_failed"
scaleset_created = "scaleset_created"
scaleset_deleted = "scaleset_deleted"
scaleset_failed = "scaleset_failed"
task_created = "task_created"
task_failed = "task_failed"
task_state_updated = "task_state_updated"
task_stopped = "task_stopped"
crash_reported = "crash_reported"
file_added = "file_added"
EventTypeMap = {
EventType.job_created: EventJobCreated,
EventType.job_stopped: EventJobStopped,
EventType.node_created: EventNodeCreated,
EventType.node_deleted: EventNodeDeleted,
EventType.node_state_updated: EventNodeStateUpdated,
EventType.ping: EventPing,
EventType.pool_created: EventPoolCreated,
EventType.pool_deleted: EventPoolDeleted,
EventType.proxy_created: EventProxyCreated,
EventType.proxy_deleted: EventProxyDeleted,
EventType.proxy_failed: EventProxyFailed,
EventType.scaleset_created: EventScalesetCreated,
EventType.scaleset_deleted: EventScalesetDeleted,
EventType.scaleset_failed: EventScalesetFailed,
EventType.task_created: EventTaskCreated,
EventType.task_failed: EventTaskFailed,
EventType.task_state_updated: EventTaskStateUpdated,
EventType.task_stopped: EventTaskStopped,
EventType.crash_reported: EventCrashReported,
EventType.file_added: EventFileAdded,
}
def get_event_type(event: Event) -> EventType:
for (event_type, event_class) in EventTypeMap.items():
if isinstance(event, event_class):
return event_type
raise NotImplementedError("unsupported event type: %s" % type(event))
class EventMessage(BaseEvent):
event_id: UUID = Field(default_factory=uuid4)
event_type: EventType
event: Event

View File

@ -17,8 +17,8 @@ from .enums import (
PoolState,
ScalesetState,
TaskState,
WebhookEventType,
)
from .events import EventType
from .models import AutoScaleConfig, NotificationConfig
from .primitives import Container, PoolName, Region
@ -215,7 +215,7 @@ class CanScheduleRequest(BaseRequest):
class WebhookCreate(BaseRequest):
name: str
url: AnyHttpUrl
event_types: List[WebhookEventType]
event_types: List[EventType]
secret_token: Optional[str]
@ -230,7 +230,7 @@ class WebhookGet(BaseModel):
class WebhookUpdate(BaseModel):
webhook_id: UUID
name: Optional[str]
event_types: Optional[List[WebhookEventType]]
event_types: Optional[List[EventType]]
url: Optional[AnyHttpUrl]
secret_token: Optional[str]

View File

@ -3,53 +3,17 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
from typing import List, Optional, Union
from typing import List, Optional
from uuid import UUID, uuid4
from pydantic import AnyHttpUrl, BaseModel, Field
from .enums import WebhookEventType, WebhookMessageState
from .models import Error, TaskConfig, UserInfo
from .responses import BaseResponse
from .enums import WebhookMessageState
from .events import EventMessage, EventType
class WebhookEventTaskStopped(BaseModel):
job_id: UUID
task_id: UUID
user_info: Optional[UserInfo]
class WebhookEventTaskFailed(BaseModel):
job_id: UUID
task_id: UUID
error: Error
user_info: Optional[UserInfo]
class WebhookEventTaskCreated(BaseModel):
job_id: UUID
task_id: UUID
config: TaskConfig
user_info: Optional[UserInfo]
class WebhookEventPing(BaseResponse):
ping_id: UUID = Field(default_factory=uuid4)
WebhookEvent = Union[
WebhookEventTaskCreated,
WebhookEventTaskStopped,
WebhookEventTaskFailed,
WebhookEventPing,
]
class WebhookMessage(BaseModel):
class WebhookMessage(EventMessage):
webhook_id: UUID
event_id: UUID = Field(default_factory=uuid4)
event_type: WebhookEventType
event: WebhookEvent
class WebhookMessageLog(WebhookMessage):
@ -61,5 +25,5 @@ class Webhook(BaseModel):
webhook_id: UUID = Field(default_factory=uuid4)
name: str
url: Optional[AnyHttpUrl]
event_types: List[WebhookEventType]
event_types: List[EventType]
secret_token: Optional[str]