Add EventGrid compatible webhook format (#1640)

This commit is contained in:
Stas
2022-02-11 16:39:19 -08:00
committed by GitHub
parent 65fd48b31b
commit 77dcd57b46
8 changed files with 143 additions and 15 deletions

View File

@ -21,6 +21,27 @@ Each event will be submitted via HTTP POST to the user provided URL.
}
```
## Event Grid Payload format
If webhook is set to have Event Grid message format then the payload will look as follows:
### Example
```json
[
{
"data": {
"ping_id": "00000000-0000-0000-0000-000000000000"
},
"dataVersion": "1.0.0",
"eventTime": "0001-01-01T00:00:00",
"eventType": "ping",
"id": "00000000-0000-0000-0000-000000000000",
"subject": "example"
}
]
```
## Event Types (EventType)
* [crash_reported](#crash_reported)

View File

@ -26,6 +26,21 @@ $ onefuzz webhooks create MYWEBHOOK https://contoso.com/my-custom-webhook task_c
$
```
Example creating a webhook subscription only the `task_created` events that produces webhook data in [Azure Event Grid](https://docs.microsoft.com/en-us/azure/event-grid/event-schema) compatible format:
```
$ onefuzz webhooks create MYWEBHOOK https://contoso.com/my-custom-webhook task_created --message_format event_grid
{
"webhook_id": "cc6926de-7c6f-487e-96ec-7b632d3ed52b",
"name": "MYWEBHOOK",
"event_types": [
"task_created"
]
}
$
```
### Listing existing webhooks
```

View File

@ -5,6 +5,7 @@
import datetime
import hmac
import json
import logging
from hashlib import sha512
from typing import List, Optional, Tuple
@ -16,7 +17,11 @@ from onefuzztypes.enums import ErrorCode, WebhookMessageState
from onefuzztypes.events import Event, EventMessage, EventPing, EventType
from onefuzztypes.models import Error, Result
from onefuzztypes.webhooks import Webhook as BASE_WEBHOOK
from onefuzztypes.webhooks import WebhookMessage
from onefuzztypes.webhooks import (
WebhookMessage,
WebhookMessageEventGrid,
WebhookMessageFormat,
)
from onefuzztypes.webhooks import WebhookMessageLog as BASE_WEBHOOK_MESSAGE_LOG
from pydantic import BaseModel, Field
@ -203,6 +208,7 @@ class Webhook(BASE_WEBHOOK, ORMMixin):
event_type=message_log.event_type,
event=message_log.event,
secret_token=self.secret_token,
message_format=self.message_format,
)
headers = {"Content-type": "application/json", "User-Agent": USER_AGENT}
@ -225,19 +231,36 @@ def build_message(
event_type: EventType,
event: Event,
secret_token: Optional[str] = None,
message_format: Optional[WebhookMessageFormat] = None,
) -> Tuple[bytes, Optional[str]]:
data = (
WebhookMessage(
webhook_id=webhook_id,
event_id=event_id,
event_type=event_type,
event=event,
instance_id=get_instance_id(),
instance_name=get_instance_name(),
if message_format and message_format == WebhookMessageFormat.event_grid:
decoded = [
json.loads(
WebhookMessageEventGrid(
id=event_id,
data=event,
dataVersion="1.0.0",
subject=get_instance_name(),
eventType=event_type,
eventTime=datetime.datetime.now(datetime.timezone.utc),
).json(sort_keys=True, exclude_none=True)
)
]
data = json.dumps(decoded).encode()
else:
data = (
WebhookMessage(
webhook_id=webhook_id,
event_id=event_id,
event_type=event_type,
event=event,
instance_id=get_instance_id(),
instance_name=get_instance_name(),
)
.json(sort_keys=True, exclude_none=True)
.encode()
)
.json(sort_keys=True, exclude_none=True)
.encode()
)
digest = None
if secret_token:
digest = hmac.new(secret_token.encode(), msg=data, digestmod=sha512).hexdigest()

View File

@ -51,6 +51,9 @@ def post(req: func.HttpRequest) -> func.HttpResponse:
event_types=request.event_types,
secret_token=request.secret_token,
)
if request.message_format is not None:
webhook.message_format = request.message_format
webhook.save()
webhook.url = None
@ -83,6 +86,9 @@ def patch(req: func.HttpRequest) -> func.HttpResponse:
if request.secret_token is not None:
webhook.secret_token = request.secret_token
if request.message_format is not None:
webhook.message_format = request.message_format
webhook.save()
webhook.url = None
webhook.secret_token = None

View File

@ -326,6 +326,7 @@ class Webhooks(Endpoint):
event_types: List[events.EventType],
*,
secret_token: Optional[str] = None,
message_format: Optional[webhooks.WebhookMessageFormat] = None,
) -> webhooks.Webhook:
"""Create a webhook"""
self.logger.debug("creating webhook. name: %s", name)
@ -333,7 +334,11 @@ class Webhooks(Endpoint):
"POST",
webhooks.Webhook,
data=requests.WebhookCreate(
name=name, url=url, event_types=event_types, secret_token=secret_token
name=name,
url=url,
event_types=event_types,
secret_token=secret_token,
message_format=message_format,
),
)
@ -345,6 +350,7 @@ class Webhooks(Endpoint):
url: Optional[str] = None,
event_types: Optional[List[events.EventType]] = None,
secret_token: Optional[str] = None,
message_format: Optional[webhooks.WebhookMessageFormat] = None,
) -> webhooks.Webhook:
"""Update a webhook"""
@ -362,6 +368,7 @@ class Webhooks(Endpoint):
url=url,
event_types=event_types,
secret_token=secret_token,
message_format=message_format,
),
)

View File

@ -3,6 +3,8 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
import datetime
import json
import sys
from typing import List, Optional
from uuid import UUID
@ -65,7 +67,7 @@ from onefuzztypes.models import (
UserInfo,
)
from onefuzztypes.primitives import Container, PoolName, Region
from onefuzztypes.webhooks import WebhookMessage
from onefuzztypes.webhooks import WebhookMessage, WebhookMessageEventGrid
EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
ZERO_SHA256 = "0" * len(EMPTY_SHA256)
@ -290,6 +292,25 @@ def main() -> None:
instance_name="example",
)
message_event_grid = WebhookMessageEventGrid(
dataVersion="1.0.0",
subject="example",
eventType=EventType.ping,
eventTime=datetime.datetime.min,
id=UUID(int=0),
data=EventPing(ping_id=UUID(int=0)),
)
message_event_grid_json = json.dumps(
[
json.loads(
message_event_grid.json(indent=4, exclude_none=True, sort_keys=True)
)
],
indent=4,
sort_keys=True,
)
result = ""
result += layer(
1,
@ -309,6 +330,21 @@ def main() -> None:
message.json(indent=4, exclude_none=True, sort_keys=True),
"json",
)
result += layer(
2,
"Event Grid Payload format",
"If webhook is set to have Event Grid message format then "
"the payload will look as follows:",
)
result += typed(
3,
"Example",
message_event_grid_json,
"json",
)
result += layer(2, "Event Types (EventType)")
event_map = {get_event_type(x).name: x for x in examples}

View File

@ -22,6 +22,7 @@ from .enums import (
from .events import EventType
from .models import AutoScaleConfig, InstanceConfig, NotificationConfig
from .primitives import Container, PoolName, Region
from .webhooks import WebhookMessageFormat
class BaseRequest(BaseModel):
@ -211,6 +212,7 @@ class WebhookCreate(BaseRequest):
url: AnyHttpUrl
event_types: List[EventType]
secret_token: Optional[str]
message_format: Optional[WebhookMessageFormat]
class WebhookSearch(BaseModel):
@ -227,6 +229,7 @@ class WebhookUpdate(BaseModel):
event_types: Optional[List[EventType]]
url: Optional[AnyHttpUrl]
secret_token: Optional[str]
message_format: Optional[WebhookMessageFormat]
class NodeAddSshKey(BaseModel):

View File

@ -3,19 +3,35 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
from datetime import datetime
from enum import Enum
from typing import List, Optional
from uuid import UUID, uuid4
from pydantic import AnyHttpUrl, BaseModel, Field
from .enums import WebhookMessageState
from .events import EventMessage, EventType
from .events import Event, EventMessage, EventType
class WebhookMessageFormat(Enum):
onefuzz = "onefuzz"
event_grid = "event_grid"
class WebhookMessage(EventMessage):
webhook_id: UUID
class WebhookMessageEventGrid(BaseModel):
dataVersion: str
subject: str
eventType: EventType
eventTime: datetime
id: UUID
data: Event
class WebhookMessageLog(WebhookMessage):
state: WebhookMessageState = Field(default=WebhookMessageState.queued)
try_count: int = Field(default=0)
@ -27,3 +43,4 @@ class Webhook(BaseModel):
url: Optional[AnyHttpUrl]
event_types: List[EventType]
secret_token: Optional[str]
message_format: Optional[WebhookMessageFormat]