Skip to content

Commit

Permalink
Webhooks Public API (#2790)
Browse files Browse the repository at this point in the history
# What this PR does
- Add public API for Webhooks CRUD, and GET webhook responses
- Add insight resource logs for internal and public webhook API calls
- Change public actions API to wrap Webhooks to maintain compatibility
with existing callers
 

## Which issue(s) this PR fixes

#2792 
#2793 

## Checklist

- [x] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not
required)
  • Loading branch information
mderynck authored Aug 22, 2023
1 parent 0dfa882 commit 7440a83
Show file tree
Hide file tree
Showing 16 changed files with 986 additions and 198 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added

- Public API for webhooks @mderynck ([#2790](https://github.com/grafana/oncall/pull/2790))

### Changed

- Public API for actions now wraps webhooks @mderynck ([#2790](https://github.com/grafana/oncall/pull/2790))
- Allow mobile app to access status endpoint @mderynck ([#2791](https://github.com/grafana/oncall/pull/2791))

## v1.3.26 (2023-08-22)
Expand Down
215 changes: 204 additions & 11 deletions docs/sources/oncall-api-reference/outgoing_webhooks.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
---
canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/outgoing_webhooks/
title: Outgoing webhooks HTTP API
title: Outgoing Webhooks HTTP API
weight: 700
---

# Outgoing webhooks (actions)
# Outgoing Webhooks

Used in escalation policies with type `trigger_action`.
> ⚠️ A note about actions: Before version **v1.3.11** webhooks existed as actions within the API, the /actions
> endpoint remains available and is compatible with previous callers but under the hood it will interact with the
> new webhooks objects. It is recommended to use the /webhooks endpoint going forward which has more features.
## List actions
For more details about specific fields of a webhook see [outgoing webhooks][outgoing-webhooks] documentation.

## List webhooks

```shell
curl "{{API_URL}}/api/v1/actions/" \
curl "{{API_URL}}/api/v1/webhooks/" \
--request GET \
--header "Authorization: meowmeowmeow" \
--header "Content-Type: application/json"
Expand All @@ -21,21 +25,210 @@ The above command returns JSON structured in the following way:

```json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": "KGEFG74LU1D8L",
"name": "Publish alert group notification to JIRA"
"id": "{{WEBHOOK_UID}}",
"name": "Demo Webhook",
"is_webhook_enabled": true,
"team": null,
"data": "{\"labels\" : {{ alert_payload.commonLabels | tojson()}}}",
"username": null,
"password": null,
"authorization_header": "****************",
"trigger_template": null,
"headers": null,
"url": "https://example.com",
"forward_all": false,
"http_method": "POST",
"trigger_type": "acknowledge",
"integration_filter": [
"CRV8A5MXC751A"
]
}
],
"current_page_number": 1,
"page_size": 50,
"count": 1,
"current_page_number": 1,
"total_pages": 1
}
```

**HTTP request**
## Get webhook

```shell
curl "{{API_URL}}/api/v1/webhooks/{{WEBHOOK_UID}}/" \
--request GET \
--header "Authorization: meowmeowmeow" \
--header "Content-Type: application/json"
```

The above command returns JSON structured in the following way:

```json
{
"id": "{{WEBHOOK_UID}}",
"name": "Demo Webhook",
"is_webhook_enabled": true,
"team": null,
"data": "{\"labels\" : {{ alert_payload.commonLabels | tojson()}}}",
"username": null,
"password": null,
"authorization_header": "****************",
"trigger_template": null,
"headers": null,
"url": "https://example.com",
"forward_all": false,
"http_method": "POST",
"trigger_type": "acknowledge",
"integration_filter": [
"CRV8A5MXC751A"
]
}
```

## Create webhook

```shell
curl "{{API_URL}}/api/v1/webhooks/" \
--request POST \
--header "Authorization: meowmeowmeow" \
--header "Content-Type: application/json" \
--data '{
"name": "New Webhook",
"url": "https://example.com",
"http_method": "POST",
"trigger_type" : "resolve"
}'
```

### Trigger Types

See [here](outgoing-webhooks#event-types) for details

- `escalation`
- `alert group created`
- `acknowledge`
- `resolve`
- `silence`
- `unsilence`
- `unresolve`
- `unacknowledge`

### HTTP Methods

- `POST`
- `GET`
- `PUT`
- `DELETE`
- `OPTIONS`

The above command returns JSON structured in the following way:

```json
{
"id": "{{WEBHOOK_UID}}",
"name": "New Webhook",
"is_webhook_enabled": true,
"team": null,
"data": null,
"username": null,
"password": null,
"authorization_header": null,
"trigger_template": null,
"headers": null,
"url": "https://example.com",
"forward_all": true,
"http_method": "POST",
"trigger_type": "resolve",
"integration_filter": null
}
```

## Update webhook

```shell
curl "{{API_URL}}/api/v1/webhooks/{{WEBHOOK_UID}}/" \
--request PUT \
--header "Authorization: meowmeowmeow" \
--header "Content-Type: application/json" \
--data '{
"is_webhook_enabled": false
}'
```

The above command returns JSON structured in the following way:

```json
{
"id": "{{WEBHOOK_UID}}",
"name": "New Webhook",
"is_webhook_enabled": false,
"team": null,
"data": null,
"username": null,
"password": null,
"authorization_header": null,
"trigger_template": null,
"headers": null,
"url": "https://example.com",
"forward_all": true,
"http_method": "POST",
"trigger_type": "resolve",
"integration_filter": null
}
```

## Delete webhook

```shell
curl "{{API_URL}}/api/v1/webhooks/{{WEBHOOK_UID}}/" \
--request DELETE \
--header "Authorization: meowmeowmeow" \
--header "Content-Type: application/json"
```

## Get webhook responses

```shell
curl "{{API_URL}}/api/v1/webhooks/{{WEBHOOK_UID}}/responses" \
--request GET \
--header "Authorization: meowmeowmeow" \
--header "Content-Type: application/json"
```

The above command returns JSON structured in the following way:

`GET {{API_URL}}/api/v1/actions/`
```json
{
"next": null,
"previous": null,
"results": [
{
"timestamp": "2023-08-18T16:38:23.106015Z",
"url": "https://example.com",
"request_trigger": "",
"request_headers": "{\"Authorization\": \"****************\"}",
"request_data": "{\"labels\": {\"alertname\": \"InstanceDown\", \"job\": \"node\", \"severity\": \"critical\"}}",
"status_code": 200,
"content": "",
"event_data": "{\"event\": {\"type\": \"acknowledge\", \"time\": \"2023-08-18T16:38:21.442981+00:00\"}, \"user\": {\"id\": \"UK49JJNPZMFLJ\", \"username\": \"oncall\", \"email\": \"admin@localhost\"}, \"alert_group\": {\"id\": \"IZQERPWKWCGH1\", \"integration_id\": \"CRV8A5MXC751A\", \"route_id\": \"RWNCT6C77M3WM\", \"alerts_count\": 1, \"state\": \"acknowledged\", \"created_at\": \"2023-08-18T16:34:27.678406Z\", \"resolved_at\": null, \"acknowledged_at\": \"2023-08-18T16:38:21.442981Z\", \"title\": \"[firing:2] InstanceDown \", \"permalinks\": {\"slack\": null, \"telegram\": null, \"web\": \"http://localhost:3000/a/grafana-oncall-app/alert-groups/IZQERPWKWCGH1\"}}, \"alert_group_id\": \"IZQERPWKWCGH1\", \"alert_payload\": {\"alerts\": [{\"endsAt\": \"0001-01-01T00:00:00Z\", \"labels\": {\"job\": \"node\", \"group\": \"production\", \"instance\": \"localhost:8081\", \"severity\": \"critical\", \"alertname\": \"InstanceDown\"}, \"status\": \"firing\", \"startsAt\": \"2023-06-12T08:24:38.326Z\", \"annotations\": {\"title\": \"Instance localhost:8081 down\", \"description\": \"localhost:8081 of job node has been down for more than 1 minute.\"}, \"fingerprint\": \"f404ecabc8dd5cd7\", \"generatorURL\": \"\"}, {\"endsAt\": \"0001-01-01T00:00:00Z\", \"labels\": {\"job\": \"node\", \"group\": \"canary\", \"instance\": \"localhost:8082\", \"severity\": \"critical\", \"alertname\": \"InstanceDown\"}, \"status\": \"firing\", \"startsAt\": \"2023-06-12T08:24:38.326Z\", \"annotations\": {\"title\": \"Instance localhost:8082 down\", \"description\": \"localhost:8082 of job node has been down for more than 1 minute.\"}, \"fingerprint\": \"f8f08d4e32c61a9d\", \"generatorURL\": \"\"}], \"status\": \"firing\", \"version\": \"4\", \"groupKey\": \"{}:{alertname=\\\"InstanceDown\\\"}\", \"receiver\": \"combo\", \"numFiring\": 2, \"externalURL\": \"\", \"groupLabels\": {\"alertname\": \"InstanceDown\"}, \"numResolved\": 0, \"commonLabels\": {\"job\": \"node\", \"severity\": \"critical\", \"alertname\": \"InstanceDown\"}, \"truncatedAlerts\": 0, \"commonAnnotations\": {}}, \"integration\": {\"id\": \"CRV8A5MXC751A\", \"type\": \"alertmanager\", \"name\": \"One - Alertmanager\", \"team\": null}, \"notified_users\": [], \"users_to_be_notified\": []}"
},
{
"timestamp": "2023-08-18T16:34:38.580574Z",
"url": "https://example.com",
"request_trigger": "",
"request_headers": null,
"request_data": "Data - Template Warning: Object of type Undefined is not JSON serializable",
"status_code": null,
"content": null,
"event_data": "{\"event\": {\"type\": \"acknowledge\", \"time\": \"2023-08-18T16:34:37.940655+00:00\"}, \"user\": {\"id\": \"UK49JJNPZMFLJ\", \"username\": \"oncall\", \"email\": \"admin@localhost\"}, \"alert_group\": {\"id\": \"IZQERPWKWCGH1\", \"integration_id\": \"CRV8A5MXC751A\", \"route_id\": \"RWNCT6C77M3WM\", \"alerts_count\": 1, \"state\": \"acknowledged\", \"created_at\": \"2023-08-18T16:34:27.678406Z\", \"resolved_at\": null, \"acknowledged_at\": \"2023-08-18T16:34:37.940655Z\", \"title\": \"[firing:2] InstanceDown \", \"permalinks\": {\"slack\": null, \"telegram\": null, \"web\": \"http://localhost:3000/a/grafana-oncall-app/alert-groups/IZQERPWKWCGH1\"}}, \"alert_group_id\": \"IZQERPWKWCGH1\", \"alert_payload\": {\"alerts\": [{\"endsAt\": \"0001-01-01T00:00:00Z\", \"labels\": {\"job\": \"node\", \"group\": \"production\", \"instance\": \"localhost:8081\", \"severity\": \"critical\", \"alertname\": \"InstanceDown\"}, \"status\": \"firing\", \"startsAt\": \"2023-06-12T08:24:38.326Z\", \"annotations\": {\"title\": \"Instance localhost:8081 down\", \"description\": \"localhost:8081 of job node has been down for more than 1 minute.\"}, \"fingerprint\": \"f404ecabc8dd5cd7\", \"generatorURL\": \"\"}, {\"endsAt\": \"0001-01-01T00:00:00Z\", \"labels\": {\"job\": \"node\", \"group\": \"canary\", \"instance\": \"localhost:8082\", \"severity\": \"critical\", \"alertname\": \"InstanceDown\"}, \"status\": \"firing\", \"startsAt\": \"2023-06-12T08:24:38.326Z\", \"annotations\": {\"title\": \"Instance localhost:8082 down\", \"description\": \"localhost:8082 of job node has been down for more than 1 minute.\"}, \"fingerprint\": \"f8f08d4e32c61a9d\", \"generatorURL\": \"\"}], \"status\": \"firing\", \"version\": \"4\", \"groupKey\": \"{}:{alertname=\\\"InstanceDown\\\"}\", \"receiver\": \"combo\", \"numFiring\": 2, \"externalURL\": \"\", \"groupLabels\": {\"alertname\": \"InstanceDown\"}, \"numResolved\": 0, \"commonLabels\": {\"job\": \"node\", \"severity\": \"critical\", \"alertname\": \"InstanceDown\"}, \"truncatedAlerts\": 0, \"commonAnnotations\": {}}, \"integration\": {\"id\": \"CRV8A5MXC751A\", \"type\": \"alertmanager\", \"name\": \"One - Alertmanager\", \"team\": null}, \"notified_users\": [], \"users_to_be_notified\": []}"
}
],
"page_size": 50,
"count": 2,
"current_page_number": 1,
"total_pages": 1
}
```
2 changes: 1 addition & 1 deletion docs/sources/outgoing-webhooks/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ To fix change the template to:

```json
{
"labels": "{{ alert_payload.labels | tojson()}}"
"labels": {{ alert_payload.labels | tojson()}}
}
```

Expand Down
1 change: 0 additions & 1 deletion engine/apps/api/serializers/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ class Meta:
"is_webhook_enabled",
"is_legacy",
"team",
"data",
"user",
"username",
"password",
Expand Down
8 changes: 4 additions & 4 deletions engine/apps/api/tests/test_webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ def test_get_list_webhooks(webhook_internal_api_setup, make_user_auth_headers):
"event_data": "",
},
"trigger_template": None,
"trigger_type": None,
"trigger_type_name": "",
"trigger_type": "0",
"trigger_type_name": "Escalation step",
}
]

Expand Down Expand Up @@ -106,8 +106,8 @@ def test_get_detail_webhook(webhook_internal_api_setup, make_user_auth_headers):
"event_data": "",
},
"trigger_template": None,
"trigger_type": None,
"trigger_type_name": "",
"trigger_type": "0",
"trigger_type_name": "Escalation step",
}

response = client.get(url, format="json", **make_user_auth_headers(user, token))
Expand Down
25 changes: 25 additions & 0 deletions engine/apps/api/views/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from common.api_helpers.exceptions import BadRequest
from common.api_helpers.filters import ByTeamModelFieldFilterMixin, ModelFieldFilterMixin, TeamModelMultipleChoiceFilter
from common.api_helpers.mixins import PublicPrimaryKeyMixin, TeamFilteringMixin
from common.insight_log import EntityEvent, write_resource_insight_log
from common.jinja_templater.apply_jinja_template import JinjaTemplateError, JinjaTemplateWarning

NEW_WEBHOOK_PK = "new"
Expand Down Expand Up @@ -60,6 +61,30 @@ class WebhooksView(TeamFilteringMixin, PublicPrimaryKeyMixin, ModelViewSet):
search_fields = ["public_primary_key", "name"]
filterset_class = WebhooksFilter

def perform_create(self, serializer):
serializer.save()
write_resource_insight_log(instance=serializer.instance, author=self.request.user, event=EntityEvent.CREATED)

def perform_update(self, serializer):
prev_state = serializer.instance.insight_logs_serialized
serializer.save()
new_state = serializer.instance.insight_logs_serialized
write_resource_insight_log(
instance=serializer.instance,
author=self.request.user,
event=EntityEvent.UPDATED,
prev_state=prev_state,
new_state=new_state,
)

def perform_destroy(self, instance):
write_resource_insight_log(
instance=instance,
author=self.request.user,
event=EntityEvent.DELETED,
)
instance.delete()

def get_queryset(self, ignore_filtering_by_available_teams=False):
queryset = Webhook.objects.filter(
organization=self.request.auth.organization,
Expand Down
Loading

0 comments on commit 7440a83

Please sign in to comment.