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) ## Event Types (EventType)
* [crash_reported](#crash_reported) * [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 ### Listing existing webhooks
``` ```

View File

@ -5,6 +5,7 @@
import datetime import datetime
import hmac import hmac
import json
import logging import logging
from hashlib import sha512 from hashlib import sha512
from typing import List, Optional, Tuple 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.events import Event, EventMessage, EventPing, EventType
from onefuzztypes.models import Error, Result from onefuzztypes.models import Error, Result
from onefuzztypes.webhooks import Webhook as BASE_WEBHOOK 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 onefuzztypes.webhooks import WebhookMessageLog as BASE_WEBHOOK_MESSAGE_LOG
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@ -203,6 +208,7 @@ class Webhook(BASE_WEBHOOK, ORMMixin):
event_type=message_log.event_type, event_type=message_log.event_type,
event=message_log.event, event=message_log.event,
secret_token=self.secret_token, secret_token=self.secret_token,
message_format=self.message_format,
) )
headers = {"Content-type": "application/json", "User-Agent": USER_AGENT} headers = {"Content-type": "application/json", "User-Agent": USER_AGENT}
@ -225,19 +231,36 @@ def build_message(
event_type: EventType, event_type: EventType,
event: Event, event: Event,
secret_token: Optional[str] = None, secret_token: Optional[str] = None,
message_format: Optional[WebhookMessageFormat] = None,
) -> Tuple[bytes, Optional[str]]: ) -> Tuple[bytes, Optional[str]]:
data = (
WebhookMessage( if message_format and message_format == WebhookMessageFormat.event_grid:
webhook_id=webhook_id, decoded = [
event_id=event_id, json.loads(
event_type=event_type, WebhookMessageEventGrid(
event=event, id=event_id,
instance_id=get_instance_id(), data=event,
instance_name=get_instance_name(), 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 digest = None
if secret_token: if secret_token:
digest = hmac.new(secret_token.encode(), msg=data, digestmod=sha512).hexdigest() 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, event_types=request.event_types,
secret_token=request.secret_token, secret_token=request.secret_token,
) )
if request.message_format is not None:
webhook.message_format = request.message_format
webhook.save() webhook.save()
webhook.url = None webhook.url = None
@ -83,6 +86,9 @@ def patch(req: func.HttpRequest) -> func.HttpResponse:
if request.secret_token is not None: if request.secret_token is not None:
webhook.secret_token = request.secret_token webhook.secret_token = request.secret_token
if request.message_format is not None:
webhook.message_format = request.message_format
webhook.save() webhook.save()
webhook.url = None webhook.url = None
webhook.secret_token = None webhook.secret_token = None

View File

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

View File

@ -3,6 +3,8 @@
# Copyright (c) Microsoft Corporation. All rights reserved. # Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. # Licensed under the MIT License.
import datetime
import json
import sys import sys
from typing import List, Optional from typing import List, Optional
from uuid import UUID from uuid import UUID
@ -65,7 +67,7 @@ from onefuzztypes.models import (
UserInfo, UserInfo,
) )
from onefuzztypes.primitives import Container, PoolName, Region from onefuzztypes.primitives import Container, PoolName, Region
from onefuzztypes.webhooks import WebhookMessage from onefuzztypes.webhooks import WebhookMessage, WebhookMessageEventGrid
EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
ZERO_SHA256 = "0" * len(EMPTY_SHA256) ZERO_SHA256 = "0" * len(EMPTY_SHA256)
@ -290,6 +292,25 @@ def main() -> None:
instance_name="example", 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 = ""
result += layer( result += layer(
1, 1,
@ -309,6 +330,21 @@ def main() -> None:
message.json(indent=4, exclude_none=True, sort_keys=True), message.json(indent=4, exclude_none=True, sort_keys=True),
"json", "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)") result += layer(2, "Event Types (EventType)")
event_map = {get_event_type(x).name: x for x in examples} 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 .events import EventType
from .models import AutoScaleConfig, InstanceConfig, NotificationConfig from .models import AutoScaleConfig, InstanceConfig, NotificationConfig
from .primitives import Container, PoolName, Region from .primitives import Container, PoolName, Region
from .webhooks import WebhookMessageFormat
class BaseRequest(BaseModel): class BaseRequest(BaseModel):
@ -211,6 +212,7 @@ class WebhookCreate(BaseRequest):
url: AnyHttpUrl url: AnyHttpUrl
event_types: List[EventType] event_types: List[EventType]
secret_token: Optional[str] secret_token: Optional[str]
message_format: Optional[WebhookMessageFormat]
class WebhookSearch(BaseModel): class WebhookSearch(BaseModel):
@ -227,6 +229,7 @@ class WebhookUpdate(BaseModel):
event_types: Optional[List[EventType]] event_types: Optional[List[EventType]]
url: Optional[AnyHttpUrl] url: Optional[AnyHttpUrl]
secret_token: Optional[str] secret_token: Optional[str]
message_format: Optional[WebhookMessageFormat]
class NodeAddSshKey(BaseModel): class NodeAddSshKey(BaseModel):

View File

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