mirror of
https://github.com/healthchecks/healthchecks.git
synced 2025-04-07 14:15:34 +00:00
Webhooks support PUT method.
.Webhooks can have different request bodies and headers for "up" and "events".
This commit is contained in:
parent
8f6726d1ee
commit
d054970b02
11 changed files with 486 additions and 276 deletions
|
@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file.
|
||||||
### Improvements
|
### Improvements
|
||||||
- Add the `prunetokenbucket` management command
|
- Add the `prunetokenbucket` management command
|
||||||
- Show check counts in JSON "badges" (#251)
|
- Show check counts in JSON "badges" (#251)
|
||||||
|
- Webhooks support PUT method (#249)
|
||||||
|
- Webhooks can have different request bodies and headers for "up" and "events" (#249)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
- Fix badges for tags containing special characters (#240, #237)
|
- Fix badges for tags containing special characters (#240, #237)
|
||||||
|
|
|
@ -360,44 +360,62 @@ class Channel(models.Model):
|
||||||
prio = int(parts[1])
|
prio = int(parts[1])
|
||||||
return PO_PRIORITIES[prio]
|
return PO_PRIORITIES[prio]
|
||||||
|
|
||||||
@property
|
def webhook_spec(self, status):
|
||||||
def url_down(self):
|
|
||||||
assert self.kind == "webhook"
|
assert self.kind == "webhook"
|
||||||
|
|
||||||
if not self.value.startswith("{"):
|
if not self.value.startswith("{"):
|
||||||
parts = self.value.split("\n")
|
parts = self.value.split("\n")
|
||||||
return parts[0]
|
url_down = parts[0]
|
||||||
|
url_up = parts[1] if len(parts) > 1 else ""
|
||||||
|
post_data = parts[2] if len(parts) > 2 else ""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"method": "POST" if post_data else "GET",
|
||||||
|
"url": url_down if status == "down" else url_up,
|
||||||
|
"body": post_data,
|
||||||
|
"headers": {},
|
||||||
|
}
|
||||||
|
|
||||||
doc = json.loads(self.value)
|
doc = json.loads(self.value)
|
||||||
return doc.get("url_down")
|
if "post_data" in doc:
|
||||||
|
# Legacy "post_data" in doc -- use the legacy fields
|
||||||
|
return {
|
||||||
|
"method": "POST" if doc["post_data"] else "GET",
|
||||||
|
"url": doc["url_down"] if status == "down" else doc["url_up"],
|
||||||
|
"body": doc["post_data"],
|
||||||
|
"headers": doc["headers"],
|
||||||
|
}
|
||||||
|
|
||||||
|
if status == "down" and "method_down" in doc:
|
||||||
|
return {
|
||||||
|
"method": doc["method_down"],
|
||||||
|
"url": doc["url_down"],
|
||||||
|
"body": doc["body_down"],
|
||||||
|
"headers": doc["headers_down"],
|
||||||
|
}
|
||||||
|
elif status == "up" and "method_up" in doc:
|
||||||
|
return {
|
||||||
|
"method": doc["method_up"],
|
||||||
|
"url": doc["url_up"],
|
||||||
|
"body": doc["body_up"],
|
||||||
|
"headers": doc["headers_up"],
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def down_webhook_spec(self):
|
||||||
|
return self.webhook_spec("down")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def up_webhook_spec(self):
|
||||||
|
return self.webhook_spec("up")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_down(self):
|
||||||
|
return self.down_webhook_spec["url"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url_up(self):
|
def url_up(self):
|
||||||
assert self.kind == "webhook"
|
return self.up_webhook_spec["url"]
|
||||||
if not self.value.startswith("{"):
|
|
||||||
parts = self.value.split("\n")
|
|
||||||
return parts[1] if len(parts) > 1 else ""
|
|
||||||
|
|
||||||
doc = json.loads(self.value)
|
|
||||||
return doc.get("url_up")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def post_data(self):
|
|
||||||
assert self.kind == "webhook"
|
|
||||||
if not self.value.startswith("{"):
|
|
||||||
parts = self.value.split("\n")
|
|
||||||
return parts[2] if len(parts) > 2 else ""
|
|
||||||
|
|
||||||
doc = json.loads(self.value)
|
|
||||||
return doc.get("post_data")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def headers(self):
|
|
||||||
assert self.kind == "webhook"
|
|
||||||
if not self.value.startswith("{"):
|
|
||||||
return {}
|
|
||||||
|
|
||||||
doc = json.loads(self.value)
|
|
||||||
return doc.get("headers", {})
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def slack_team(self):
|
def slack_team(self):
|
||||||
|
|
|
@ -146,8 +146,8 @@ class NotifyTestCase(BaseTestCase):
|
||||||
self.assertIsInstance(kwargs["data"], bytes)
|
self.assertIsInstance(kwargs["data"], bytes)
|
||||||
|
|
||||||
@patch("hc.api.transports.requests.request")
|
@patch("hc.api.transports.requests.request")
|
||||||
def test_webhooks_handle_json_value(self, mock_request):
|
def test_legacy_webhooks_handle_json_value(self, mock_request):
|
||||||
definition = {"url_down": "http://foo.com"}
|
definition = {"url_down": "http://foo.com", "post_data": "", "headers": {}}
|
||||||
self._setup_data("webhook", json.dumps(definition))
|
self._setup_data("webhook", json.dumps(definition))
|
||||||
self.channel.notify(self.check)
|
self.channel.notify(self.check)
|
||||||
|
|
||||||
|
@ -157,8 +157,8 @@ class NotifyTestCase(BaseTestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch("hc.api.transports.requests.request")
|
@patch("hc.api.transports.requests.request")
|
||||||
def test_webhooks_handle_json_up_event(self, mock_request):
|
def test_legacy_webhooks_handle_json_up_event(self, mock_request):
|
||||||
definition = {"url_up": "http://bar"}
|
definition = {"url_up": "http://bar", "post_data": "", "headers": {}}
|
||||||
|
|
||||||
self._setup_data("webhook", json.dumps(definition), status="up")
|
self._setup_data("webhook", json.dumps(definition), status="up")
|
||||||
self.channel.notify(self.check)
|
self.channel.notify(self.check)
|
||||||
|
@ -167,7 +167,22 @@ class NotifyTestCase(BaseTestCase):
|
||||||
mock_request.assert_called_with("get", "http://bar", headers=headers, timeout=5)
|
mock_request.assert_called_with("get", "http://bar", headers=headers, timeout=5)
|
||||||
|
|
||||||
@patch("hc.api.transports.requests.request")
|
@patch("hc.api.transports.requests.request")
|
||||||
def test_webhooks_handle_post_headers(self, mock_request):
|
def test_webhooks_handle_json_up_event(self, mock_request):
|
||||||
|
definition = {
|
||||||
|
"method_up": "GET",
|
||||||
|
"url_up": "http://bar",
|
||||||
|
"body_up": "",
|
||||||
|
"headers_up": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
self._setup_data("webhook", json.dumps(definition), status="up")
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
|
||||||
|
headers = {"User-Agent": "healthchecks.io"}
|
||||||
|
mock_request.assert_called_with("get", "http://bar", headers=headers, timeout=5)
|
||||||
|
|
||||||
|
@patch("hc.api.transports.requests.request")
|
||||||
|
def test_legacy_webhooks_handle_post_headers(self, mock_request):
|
||||||
definition = {
|
definition = {
|
||||||
"url_down": "http://foo.com",
|
"url_down": "http://foo.com",
|
||||||
"post_data": "data",
|
"post_data": "data",
|
||||||
|
@ -183,9 +198,27 @@ class NotifyTestCase(BaseTestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch("hc.api.transports.requests.request")
|
@patch("hc.api.transports.requests.request")
|
||||||
def test_webhooks_handle_get_headers(self, mock_request):
|
def test_webhooks_handle_post_headers(self, mock_request):
|
||||||
|
definition = {
|
||||||
|
"method_down": "POST",
|
||||||
|
"url_down": "http://foo.com",
|
||||||
|
"body_down": "data",
|
||||||
|
"headers_down": {"Content-Type": "application/json"},
|
||||||
|
}
|
||||||
|
|
||||||
|
self._setup_data("webhook", json.dumps(definition))
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
|
||||||
|
headers = {"User-Agent": "healthchecks.io", "Content-Type": "application/json"}
|
||||||
|
mock_request.assert_called_with(
|
||||||
|
"post", "http://foo.com", data=b"data", headers=headers, timeout=5
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("hc.api.transports.requests.request")
|
||||||
|
def test_legacy_webhooks_handle_get_headers(self, mock_request):
|
||||||
definition = {
|
definition = {
|
||||||
"url_down": "http://foo.com",
|
"url_down": "http://foo.com",
|
||||||
|
"post_data": "",
|
||||||
"headers": {"Content-Type": "application/json"},
|
"headers": {"Content-Type": "application/json"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,9 +231,27 @@ class NotifyTestCase(BaseTestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch("hc.api.transports.requests.request")
|
@patch("hc.api.transports.requests.request")
|
||||||
def test_webhooks_allow_user_agent_override(self, mock_request):
|
def test_webhooks_handle_get_headers(self, mock_request):
|
||||||
|
definition = {
|
||||||
|
"method_down": "GET",
|
||||||
|
"url_down": "http://foo.com",
|
||||||
|
"body_down": "",
|
||||||
|
"headers_down": {"Content-Type": "application/json"},
|
||||||
|
}
|
||||||
|
|
||||||
|
self._setup_data("webhook", json.dumps(definition))
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
|
||||||
|
headers = {"User-Agent": "healthchecks.io", "Content-Type": "application/json"}
|
||||||
|
mock_request.assert_called_with(
|
||||||
|
"get", "http://foo.com", headers=headers, timeout=5
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("hc.api.transports.requests.request")
|
||||||
|
def test_legacy_webhooks_allow_user_agent_override(self, mock_request):
|
||||||
definition = {
|
definition = {
|
||||||
"url_down": "http://foo.com",
|
"url_down": "http://foo.com",
|
||||||
|
"post_data": "",
|
||||||
"headers": {"User-Agent": "My-Agent"},
|
"headers": {"User-Agent": "My-Agent"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,11 +263,30 @@ class NotifyTestCase(BaseTestCase):
|
||||||
"get", "http://foo.com", headers=headers, timeout=5
|
"get", "http://foo.com", headers=headers, timeout=5
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@patch("hc.api.transports.requests.request")
|
||||||
|
def test_webhooks_allow_user_agent_override(self, mock_request):
|
||||||
|
definition = {
|
||||||
|
"method_down": "GET",
|
||||||
|
"url_down": "http://foo.com",
|
||||||
|
"body_down": "",
|
||||||
|
"headers_down": {"User-Agent": "My-Agent"},
|
||||||
|
}
|
||||||
|
|
||||||
|
self._setup_data("webhook", json.dumps(definition))
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
|
||||||
|
headers = {"User-Agent": "My-Agent"}
|
||||||
|
mock_request.assert_called_with(
|
||||||
|
"get", "http://foo.com", headers=headers, timeout=5
|
||||||
|
)
|
||||||
|
|
||||||
@patch("hc.api.transports.requests.request")
|
@patch("hc.api.transports.requests.request")
|
||||||
def test_webhooks_support_variables_in_headers(self, mock_request):
|
def test_webhooks_support_variables_in_headers(self, mock_request):
|
||||||
definition = {
|
definition = {
|
||||||
|
"method_down": "GET",
|
||||||
"url_down": "http://foo.com",
|
"url_down": "http://foo.com",
|
||||||
"headers": {"X-Message": "$NAME is DOWN"},
|
"body_down": "",
|
||||||
|
"headers_down": {"X-Message": "$NAME is DOWN"},
|
||||||
}
|
}
|
||||||
|
|
||||||
self._setup_data("webhook", json.dumps(definition))
|
self._setup_data("webhook", json.dumps(definition))
|
||||||
|
|
|
@ -178,22 +178,24 @@ class Webhook(HttpTransport):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def notify(self, check):
|
def notify(self, check):
|
||||||
url = self.channel.url_down
|
spec = self.channel.webhook_spec(check.status)
|
||||||
if check.status == "up":
|
assert spec["url"]
|
||||||
url = self.channel.url_up
|
|
||||||
|
|
||||||
assert url
|
url = self.prepare(spec["url"], check, urlencode=True)
|
||||||
|
|
||||||
url = self.prepare(url, check, urlencode=True)
|
|
||||||
headers = {}
|
headers = {}
|
||||||
for key, value in self.channel.headers.items():
|
for key, value in spec["headers"].items():
|
||||||
headers[key] = self.prepare(value, check)
|
headers[key] = self.prepare(value, check)
|
||||||
|
|
||||||
if self.channel.post_data:
|
body = spec["body"]
|
||||||
payload = self.prepare(self.channel.post_data, check)
|
if body:
|
||||||
return self.post(url, data=payload.encode(), headers=headers)
|
body = self.prepare(body, check)
|
||||||
else:
|
|
||||||
|
if spec["method"] == "GET":
|
||||||
return self.get(url, headers=headers)
|
return self.get(url, headers=headers)
|
||||||
|
elif spec["method"] == "POST":
|
||||||
|
return self.post(url, data=body.encode(), headers=headers)
|
||||||
|
elif spec["method"] == "PUT":
|
||||||
|
return self.put(url, data=body.encode(), headers=headers)
|
||||||
|
|
||||||
|
|
||||||
class Slack(HttpTransport):
|
class Slack(HttpTransport):
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
from datetime import timedelta as td
|
from datetime import timedelta as td
|
||||||
import json
|
import json
|
||||||
import re
|
|
||||||
from urllib.parse import quote, urlencode
|
from urllib.parse import quote, urlencode
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.forms import URLField
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
from hc.front.validators import (
|
from hc.front.validators import (
|
||||||
CronExpressionValidator,
|
CronExpressionValidator,
|
||||||
|
@ -14,6 +15,37 @@ from hc.front.validators import (
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
class HeadersField(forms.Field):
|
||||||
|
message = """Use "Header-Name: value" pairs, one per line."""
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
if not value:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
headers = {}
|
||||||
|
for line in value.split("\n"):
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
|
||||||
|
if ":" not in value:
|
||||||
|
raise ValidationError(self.message)
|
||||||
|
|
||||||
|
n, v = line.split(":", maxsplit=1)
|
||||||
|
n, v = n.strip(), v.strip()
|
||||||
|
if not n or not v:
|
||||||
|
raise ValidationError(message=self.message)
|
||||||
|
|
||||||
|
headers[n] = v
|
||||||
|
|
||||||
|
return headers
|
||||||
|
|
||||||
|
def validate(self, value):
|
||||||
|
super().validate(value)
|
||||||
|
for k, v in value.items():
|
||||||
|
if len(k) > 1000 or len(v) > 1000:
|
||||||
|
raise ValidationError("Value too long")
|
||||||
|
|
||||||
|
|
||||||
class NameTagsForm(forms.Form):
|
class NameTagsForm(forms.Form):
|
||||||
name = forms.CharField(max_length=100, required=False)
|
name = forms.CharField(max_length=100, required=False)
|
||||||
tags = forms.CharField(max_length=500, required=False)
|
tags = forms.CharField(max_length=500, required=False)
|
||||||
|
@ -68,49 +100,28 @@ class AddUrlForm(forms.Form):
|
||||||
value = forms.URLField(max_length=1000, validators=[WebhookValidator()])
|
value = forms.URLField(max_length=1000, validators=[WebhookValidator()])
|
||||||
|
|
||||||
|
|
||||||
_valid_header_name = re.compile(r"\A[^:\s][^:\r\n]*\Z").match
|
METHODS = ("GET", "POST", "PUT")
|
||||||
|
|
||||||
|
|
||||||
class AddWebhookForm(forms.Form):
|
class AddWebhookForm(forms.Form):
|
||||||
error_css_class = "has-error"
|
error_css_class = "has-error"
|
||||||
|
|
||||||
url_down = forms.URLField(
|
method_down = forms.ChoiceField(initial="GET", choices=zip(METHODS, METHODS))
|
||||||
|
body_down = forms.CharField(max_length=1000, required=False)
|
||||||
|
headers_down = HeadersField(required=False)
|
||||||
|
url_down = URLField(
|
||||||
max_length=1000, required=False, validators=[WebhookValidator()]
|
max_length=1000, required=False, validators=[WebhookValidator()]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
method_up = forms.ChoiceField(initial="GET", choices=zip(METHODS, METHODS))
|
||||||
|
body_up = forms.CharField(max_length=1000, required=False)
|
||||||
|
headers_up = HeadersField(required=False)
|
||||||
url_up = forms.URLField(
|
url_up = forms.URLField(
|
||||||
max_length=1000, required=False, validators=[WebhookValidator()]
|
max_length=1000, required=False, validators=[WebhookValidator()]
|
||||||
)
|
)
|
||||||
|
|
||||||
post_data = forms.CharField(max_length=1000, required=False)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(AddWebhookForm, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
self.invalid_header_names = set()
|
|
||||||
self.headers = {}
|
|
||||||
if "header_key[]" in self.data and "header_value[]" in self.data:
|
|
||||||
keys = self.data.getlist("header_key[]")
|
|
||||||
values = self.data.getlist("header_value[]")
|
|
||||||
for key, value in zip(keys, values):
|
|
||||||
if not key:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not _valid_header_name(key):
|
|
||||||
self.invalid_header_names.add(key)
|
|
||||||
|
|
||||||
self.headers[key] = value
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
if self.invalid_header_names:
|
|
||||||
raise forms.ValidationError("Invalid header names")
|
|
||||||
|
|
||||||
return self.cleaned_data
|
|
||||||
|
|
||||||
def get_value(self):
|
def get_value(self):
|
||||||
val = dict(self.cleaned_data)
|
return json.dumps(dict(self.cleaned_data), sort_keys=True)
|
||||||
val["headers"] = self.headers
|
|
||||||
return json.dumps(val, sort_keys=True)
|
|
||||||
|
|
||||||
|
|
||||||
phone_validator = RegexValidator(
|
phone_validator = RegexValidator(
|
||||||
|
|
|
@ -123,3 +123,8 @@ def fix_asterisks(s):
|
||||||
""" Prepend asterisks with "Combining Grapheme Joiner" characters. """
|
""" Prepend asterisks with "Combining Grapheme Joiner" characters. """
|
||||||
|
|
||||||
return s.replace("*", "\u034f*")
|
return s.replace("*", "\u034f*")
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def format_headers(headers):
|
||||||
|
return "\n".join("%s: %s" % (k, v) for k, v in headers.items())
|
||||||
|
|
|
@ -8,24 +8,32 @@ class AddWebhookTestCase(BaseTestCase):
|
||||||
def test_instructions_work(self):
|
def test_instructions_work(self):
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
r = self.client.get(self.url)
|
r = self.client.get(self.url)
|
||||||
self.assertContains(r, "Runs a HTTP GET or HTTP POST")
|
self.assertContains(r, "Executes an HTTP request")
|
||||||
|
|
||||||
def test_it_adds_two_webhook_urls_and_redirects(self):
|
def test_it_adds_two_webhook_urls_and_redirects(self):
|
||||||
form = {"url_down": "http://foo.com", "url_up": "https://bar.com"}
|
form = {
|
||||||
|
"method_down": "GET",
|
||||||
|
"url_down": "http://foo.com",
|
||||||
|
"method_up": "GET",
|
||||||
|
"url_up": "https://bar.com",
|
||||||
|
}
|
||||||
|
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
r = self.client.post(self.url, form)
|
r = self.client.post(self.url, form)
|
||||||
self.assertRedirects(r, "/integrations/")
|
self.assertRedirects(r, "/integrations/")
|
||||||
|
|
||||||
c = Channel.objects.get()
|
c = Channel.objects.get()
|
||||||
self.assertEqual(
|
|
||||||
c.value,
|
|
||||||
'{"headers": {}, "post_data": "", "url_down": "http://foo.com", "url_up": "https://bar.com"}',
|
|
||||||
)
|
|
||||||
self.assertEqual(c.project, self.project)
|
self.assertEqual(c.project, self.project)
|
||||||
|
self.assertEqual(c.down_webhook_spec["url"], "http://foo.com")
|
||||||
|
self.assertEqual(c.up_webhook_spec["url"], "https://bar.com")
|
||||||
|
|
||||||
def test_it_adds_webhook_using_team_access(self):
|
def test_it_adds_webhook_using_team_access(self):
|
||||||
form = {"url_down": "http://foo.com", "url_up": "https://bar.com"}
|
form = {
|
||||||
|
"method_down": "GET",
|
||||||
|
"url_down": "http://foo.com",
|
||||||
|
"method_up": "GET",
|
||||||
|
"url_up": "https://bar.com",
|
||||||
|
}
|
||||||
|
|
||||||
# Logging in as bob, not alice. Bob has team access so this
|
# Logging in as bob, not alice. Bob has team access so this
|
||||||
# should work.
|
# should work.
|
||||||
|
@ -34,10 +42,8 @@ class AddWebhookTestCase(BaseTestCase):
|
||||||
|
|
||||||
c = Channel.objects.get()
|
c = Channel.objects.get()
|
||||||
self.assertEqual(c.project, self.project)
|
self.assertEqual(c.project, self.project)
|
||||||
self.assertEqual(
|
self.assertEqual(c.down_webhook_spec["url"], "http://foo.com")
|
||||||
c.value,
|
self.assertEqual(c.up_webhook_spec["url"], "https://bar.com")
|
||||||
'{"headers": {}, "post_data": "", "url_down": "http://foo.com", "url_up": "https://bar.com"}',
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_it_rejects_bad_urls(self):
|
def test_it_rejects_bad_urls(self):
|
||||||
urls = [
|
urls = [
|
||||||
|
@ -52,7 +58,12 @@ class AddWebhookTestCase(BaseTestCase):
|
||||||
|
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
for url in urls:
|
for url in urls:
|
||||||
form = {"url_down": url, "url_up": ""}
|
form = {
|
||||||
|
"method_down": "GET",
|
||||||
|
"url_down": url,
|
||||||
|
"method_up": "GET",
|
||||||
|
"url_up": "",
|
||||||
|
}
|
||||||
|
|
||||||
r = self.client.post(self.url, form)
|
r = self.client.post(self.url, form)
|
||||||
self.assertContains(r, "Enter a valid URL.", msg_prefix=url)
|
self.assertContains(r, "Enter a valid URL.", msg_prefix=url)
|
||||||
|
@ -60,35 +71,41 @@ class AddWebhookTestCase(BaseTestCase):
|
||||||
self.assertEqual(Channel.objects.count(), 0)
|
self.assertEqual(Channel.objects.count(), 0)
|
||||||
|
|
||||||
def test_it_handles_empty_down_url(self):
|
def test_it_handles_empty_down_url(self):
|
||||||
form = {"url_down": "", "url_up": "http://foo.com"}
|
form = {
|
||||||
|
"method_down": "GET",
|
||||||
|
"url_down": "",
|
||||||
|
"method_up": "GET",
|
||||||
|
"url_up": "http://foo.com",
|
||||||
|
}
|
||||||
|
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
self.client.post(self.url, form)
|
self.client.post(self.url, form)
|
||||||
|
|
||||||
c = Channel.objects.get()
|
c = Channel.objects.get()
|
||||||
self.assertEqual(
|
self.assertEqual(c.down_webhook_spec["url"], "")
|
||||||
c.value,
|
self.assertEqual(c.up_webhook_spec["url"], "http://foo.com")
|
||||||
'{"headers": {}, "post_data": "", "url_down": "", "url_up": "http://foo.com"}',
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_it_adds_post_data(self):
|
def test_it_adds_request_body(self):
|
||||||
form = {"url_down": "http://foo.com", "post_data": "hello"}
|
form = {
|
||||||
|
"method_down": "POST",
|
||||||
|
"url_down": "http://foo.com",
|
||||||
|
"body_down": "hello",
|
||||||
|
"method_up": "GET",
|
||||||
|
}
|
||||||
|
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
r = self.client.post(self.url, form)
|
r = self.client.post(self.url, form)
|
||||||
self.assertRedirects(r, "/integrations/")
|
self.assertRedirects(r, "/integrations/")
|
||||||
|
|
||||||
c = Channel.objects.get()
|
c = Channel.objects.get()
|
||||||
self.assertEqual(
|
self.assertEqual(c.down_webhook_spec["body"], "hello")
|
||||||
c.value,
|
|
||||||
'{"headers": {}, "post_data": "hello", "url_down": "http://foo.com", "url_up": ""}',
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_it_adds_headers(self):
|
def test_it_adds_headers(self):
|
||||||
form = {
|
form = {
|
||||||
|
"method_down": "GET",
|
||||||
"url_down": "http://foo.com",
|
"url_down": "http://foo.com",
|
||||||
"header_key[]": ["test", "test2"],
|
"headers_down": "test:123\ntest2:abc",
|
||||||
"header_value[]": ["123", "abc"],
|
"method_up": "GET",
|
||||||
}
|
}
|
||||||
|
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
@ -96,16 +113,34 @@ class AddWebhookTestCase(BaseTestCase):
|
||||||
self.assertRedirects(r, "/integrations/")
|
self.assertRedirects(r, "/integrations/")
|
||||||
|
|
||||||
c = Channel.objects.get()
|
c = Channel.objects.get()
|
||||||
self.assertEqual(c.headers, {"test": "123", "test2": "abc"})
|
self.assertEqual(
|
||||||
|
c.down_webhook_spec["headers"], {"test": "123", "test2": "abc"}
|
||||||
|
)
|
||||||
|
|
||||||
def test_it_rejects_bad_header_names(self):
|
def test_it_rejects_bad_headers(self):
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
form = {
|
form = {
|
||||||
|
"method_down": "GET",
|
||||||
"url_down": "http://example.org",
|
"url_down": "http://example.org",
|
||||||
"header_key[]": ["ill:egal"],
|
"headers_down": "invalid-headers",
|
||||||
"header_value[]": ["123"],
|
"method_up": "GET",
|
||||||
}
|
}
|
||||||
|
|
||||||
r = self.client.post(self.url, form)
|
r = self.client.post(self.url, form)
|
||||||
self.assertContains(r, "Please use valid HTTP header names.")
|
self.assertContains(r, """invalid-headers""")
|
||||||
self.assertEqual(Channel.objects.count(), 0)
|
self.assertEqual(Channel.objects.count(), 0)
|
||||||
|
|
||||||
|
def test_it_strips_headers(self):
|
||||||
|
form = {
|
||||||
|
"method_down": "GET",
|
||||||
|
"url_down": "http://foo.com",
|
||||||
|
"headers_down": " test : 123 ",
|
||||||
|
"method_up": "GET",
|
||||||
|
}
|
||||||
|
|
||||||
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
r = self.client.post(self.url, form)
|
||||||
|
self.assertRedirects(r, "/integrations/")
|
||||||
|
|
||||||
|
c = Channel.objects.get()
|
||||||
|
self.assertEqual(c.down_webhook_spec["headers"], {"test": "123"})
|
||||||
|
|
|
@ -2,3 +2,39 @@
|
||||||
border-color: #a94442;
|
border-color: #a94442;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#add-webhook-form div.bootstrap-select {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-url-group {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-url-group > div.dropdown button {
|
||||||
|
border-right: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-url-group input {
|
||||||
|
z-index: 1;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#webhook-variables tr:first-child th, #webhook-variables tr:first-child td {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#webhook-variables th {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-down {
|
||||||
|
color: #d9534f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-up {
|
||||||
|
color: #5cb85c
|
||||||
|
}
|
|
@ -1,25 +1,15 @@
|
||||||
$(function() {
|
$(function() {
|
||||||
function haveBlankHeaderForm() {
|
$("#method-down").change(function() {
|
||||||
return $("#webhook-headers .webhook-header").filter(function() {
|
var method = this.value;
|
||||||
var key = $(".key", this).val();
|
$("#body-down-group").toggle(method != "GET");
|
||||||
var value = $(".value", this).val();
|
});
|
||||||
return !key && !value;
|
|
||||||
}).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureBlankHeaderForm() {
|
$("#method-up").change(function() {
|
||||||
if (!haveBlankHeaderForm()) {
|
var method = this.value;
|
||||||
var tmpl = $("#header-template").html();
|
$("#body-up-group").toggle(method != "GET");
|
||||||
$("#webhook-headers").append(tmpl);
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#webhook-headers").on("click", "button", function(e) {
|
// On page load, check if we need to show "request body" fields
|
||||||
e.preventDefault();
|
$("#method-down").trigger("change");
|
||||||
$(this).closest(".webhook-header").remove();
|
$("#method-up").trigger("change");
|
||||||
ensureBlankHeaderForm();
|
|
||||||
})
|
|
||||||
|
|
||||||
$("#webhook-headers").on("keyup", "input", ensureBlankHeaderForm);
|
|
||||||
ensureBlankHeaderForm();
|
|
||||||
});
|
});
|
|
@ -398,30 +398,38 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if ch.kind == "webhook" %}
|
{% if ch.kind == "webhook" %}
|
||||||
|
{% with ch.down_webhook_spec as spec %}
|
||||||
|
{% if spec.url %}
|
||||||
|
<p><strong>Execute on "down" events:</strong></p>
|
||||||
|
<pre>{{ spec.method }} {{ spec.url }}</pre>
|
||||||
|
{% if spec.body %}
|
||||||
|
<p>Request Body</p>
|
||||||
|
<pre>{{ spec.body }}</pre>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if ch.url_down %}
|
{% if spec.headers %}
|
||||||
<p><strong>URL for "down" events</strong></p>
|
<p>Request Headers</p>
|
||||||
<pre>{{ ch.url_down }}</pre>
|
<pre>{{ spec.headers|format_headers }}</pre>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% with ch.up_webhook_spec as spec %}
|
||||||
|
{% if spec.url %}
|
||||||
|
<p><strong>Execute on "up" events:</strong></p>
|
||||||
|
<pre>{{ spec.method }} {{ spec.url }}</pre>
|
||||||
|
{% if spec.body %}
|
||||||
|
<p>Request Body</p>
|
||||||
|
<pre>{{ spec.body }}</pre>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if spec.headers %}
|
||||||
|
<p>Request Headers</p>
|
||||||
|
<pre>{{ spec.headers|format_headers }}</pre>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if ch.url_up %}
|
|
||||||
<p><strong>URL for "up" events</strong></p>
|
|
||||||
<pre>{{ ch.url_up }}</pre>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if ch.post_data %}
|
|
||||||
<p><strong>POST data</strong></p>
|
|
||||||
<pre>{{ ch.post_data }}</pre>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
|
||||||
{% for k, v in ch.headers.items %}
|
|
||||||
<p><strong>Header <code>{{ k }}</code></strong></p>
|
|
||||||
<pre>{{ v }}</pre>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||||
|
|
|
@ -6,168 +6,201 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<h1>Webhook</h1>
|
<h1>Webhook</h1>
|
||||||
|
|
||||||
<p>Runs a HTTP GET or HTTP POST to your specified URL when a check
|
<p>Executes an HTTP request to your specified URL when a check
|
||||||
goes up or down. Uses GET by default, and uses POST if you specify
|
goes up or down.</p>
|
||||||
any POST data.</p>
|
|
||||||
|
|
||||||
<p>You can use the following variables in webhook URLs:</p>
|
<p>
|
||||||
<table class="table webhook-variables">
|
You can use placeholders <strong>$NAME</strong>, <strong>$STATUS</strong> and others in webhook URLs,
|
||||||
<tr>
|
request body and header values
|
||||||
<th class="variable-column">Variable</th>
|
<a href="#" data-toggle="modal" data-target="#reference-modal">(quick reference)</a>.
|
||||||
<td>Will be replaced with…</td>
|
</p>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th><code>$CODE</code></th>
|
|
||||||
<td>The UUID code of the check</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th><code>$NAME</code></th>
|
|
||||||
<td>Name of the check</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th><code>$NOW</code></th>
|
|
||||||
<td>
|
|
||||||
Current UTC time in ISO8601 format.
|
|
||||||
Example: "{{ now }}"
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th><code>$STATUS</code></th>
|
|
||||||
<td>Check's current status ("up" or "down")</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th><code>$TAG1, $TAG2, …</code></th>
|
|
||||||
<td>Value of the first tag, the second tag, …</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<p>For example, a callback URL using variables might look like so:
|
<form id="add-webhook-form" method="post">
|
||||||
<pre>http://requestb.in/1hhct291?message=<strong>$NAME</strong>:<strong>$STATUS</strong></pre>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
After encoding and replacing the variables, {% site_name %} would then call:
|
|
||||||
</p>
|
|
||||||
<pre>http://requestb.in/1hhct291?message=<strong>My%20Check</strong>:<strong>down</strong></pre>
|
|
||||||
|
|
||||||
<h2>Integration Settings</h2>
|
|
||||||
|
|
||||||
<form method="post" class="form-horizontal">
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="kind" value="webhook" />
|
|
||||||
<div class="form-group {{ form.url_down.css_classes }}">
|
<div class="row">
|
||||||
<label class="col-sm-2 control-label">URL for "down" events</label>
|
<div class="col-sm-6">
|
||||||
<div class="col-sm-10">
|
<h2>Execute when a check goes <strong class="label-down">down</strong></h2>
|
||||||
<input
|
<br />
|
||||||
type="text"
|
|
||||||
class="form-control"
|
<div class="form-group {{ form.url_down.css_classes }}">
|
||||||
name="url_down"
|
<label>URL</label>
|
||||||
placeholder="http://..."
|
<div class="method-url-group">
|
||||||
value="{{ form.url_down.value|default:"" }}">
|
<select id="method-down" name="method_down" class="selectpicker">
|
||||||
|
<option{% if form.method_down.value == "GET" %} selected{% endif %}>GET</option>
|
||||||
|
<option{% if form.method_down.value == "POST" %} selected{% endif %}>POST</option>
|
||||||
|
<option{% if form.method_down.value == "PUT" %} selected{% endif %}>PUT</option>
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
name="url_down"
|
||||||
|
value="{{ form.url_down.value|default:"" }}"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="https://..." />
|
||||||
|
</div>
|
||||||
{% if form.url_down.errors %}
|
{% if form.url_down.errors %}
|
||||||
<div class="help-block">
|
<div class="help-block">
|
||||||
{{ form.url_down.errors|join:"" }}
|
{{ form.url_down.errors|join:"" }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="form-group {{ form.url_up.css_classes }}">
|
<div id="body-down-group" class="form-group {{ form.body_down.css_classes }}" style="display: none">
|
||||||
<label class="col-sm-2 control-label">URL for "up" events</label>
|
<label class="control-label">Request Body</label>
|
||||||
<div class="col-sm-10">
|
<textarea
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
class="form-control"
|
||||||
name="url_up"
|
rows="3"
|
||||||
placeholder="http://..."
|
name="body_down"
|
||||||
value="{{ form.url_up.value|default:"" }}">
|
placeholder='{"status": "$STATUS"}'>{{ form.body_down.value|default:"" }}</textarea>
|
||||||
|
{% if form.body_down.errors %}
|
||||||
|
<div class="help-block">
|
||||||
|
{{ form.body_down.errors|join:"" }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group {{ form.headers_down.css_classes }}">
|
||||||
|
<label class="control-label">Request Headers</label>
|
||||||
|
<textarea
|
||||||
|
class="form-control"
|
||||||
|
rows="3"
|
||||||
|
name="headers_down"
|
||||||
|
placeholder="X-Sample-Header: $NAME has gone down">{{ form.headers_down.value|default:"" }}</textarea>
|
||||||
|
<div class="help-block">
|
||||||
|
{% if form.headers_down.errors %}
|
||||||
|
{{ form.headers_down.errors|join:"" }}
|
||||||
|
{% else %}
|
||||||
|
Optional "Header-Name: value" pairs, one pair per line.
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<h2>Execute when a check goes <strong class="label-up">up</strong></h2>
|
||||||
|
<br />
|
||||||
|
<div class="form-group {{ form.url_up.css_classes }}">
|
||||||
|
<label>URL</label>
|
||||||
|
<div class="method-url-group">
|
||||||
|
<select id="method-up" name="method_up" class="selectpicker">
|
||||||
|
<option{% if form.method_up.value == "GET" %} selected{% endif %}>GET</option>
|
||||||
|
<option{% if form.method_up.value == "POST" %} selected{% endif %}>POST</option>
|
||||||
|
<option{% if form.method_up.value == "PUT" %} selected{% endif %}>PUT</option>
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
name="url_up"
|
||||||
|
value="{{ form.url_up.value|default:"" }}"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="https://..." />
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if form.url_up.errors %}
|
{% if form.url_up.errors %}
|
||||||
<div class="help-block">
|
<div class="help-block">
|
||||||
{{ form.url_up.errors|join:"" }}
|
{{ form.url_up.errors|join:"" }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div id="body-up-group" class="form-group {{ form.body_up.css_classes }}" style="display: none">
|
||||||
<div class="form-group {{ form.post_data.css_classes }}">
|
<label class="control-label">Request Body</label>
|
||||||
<label class="col-sm-2 control-label">POST data</label>
|
<textarea
|
||||||
<div class="col-sm-10">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
class="form-control"
|
||||||
name="post_data"
|
rows="3"
|
||||||
placeholder='{"status": "$STATUS"}'
|
name="body_up"
|
||||||
value="{{ form.post_data.value|default:"" }}">
|
placeholder='{"status": "$STATUS"}'>{{ form.body_up.value|default:"" }}</textarea>
|
||||||
{% if form.post_data.errors %}
|
{% if form.post_data.errors %}
|
||||||
<div class="help-block">
|
<div class="help-block">
|
||||||
{{ form.post_data.errors|join:"" }}
|
{{ form.post_data.errors|join:"" }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group {{ form.headers_up.css_classes }}">
|
||||||
<label class="col-sm-2 control-label">Request Headers</label>
|
<label class="control-label">Request Headers</label>
|
||||||
<div class="col-xs-12 col-sm-10">
|
<textarea
|
||||||
<div id="webhook-headers">
|
class="form-control"
|
||||||
{% for k, v in form.headers.items %}
|
rows="3"
|
||||||
<div class="form-inline webhook-header">
|
name="headers_up"
|
||||||
<input
|
placeholder="X-Sample-Header: $NAME is back up">{{ form.headers_up.value|default:"" }}</textarea>
|
||||||
type="text"
|
<div class="help-block">
|
||||||
class="form-control key {% if k in form.invalid_header_names %}error{% endif %}"
|
{% if form.headers_up.errors %}
|
||||||
name="header_key[]"
|
{{ form.headers_up.errors|join:"" }}
|
||||||
placeholder="Content-Type"
|
{% else %}
|
||||||
value="{{ k }}" />
|
Optional "Header-Name: value" pairs, one pair per line.
|
||||||
<input
|
{% endif %}
|
||||||
type="text"
|
|
||||||
class="form-control value"
|
|
||||||
name="header_value[]"
|
|
||||||
placeholder="application/json"
|
|
||||||
value="{{ v }}" />
|
|
||||||
<button class="btn btn-default" type="button">
|
|
||||||
<span class="icon-delete"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
{% if form.invalid_header_names %}
|
|
||||||
<div class="text-danger">
|
|
||||||
Please use valid HTTP header names.
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
</div>
|
||||||
<div class="col-sm-offset-2 col-sm-10">
|
|
||||||
|
<div class="form-group" class="clearfix">
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<div class="text-right">
|
||||||
<button type="submit" class="btn btn-primary">Save Integration</button>
|
<button type="submit" class="btn btn-primary">Save Integration</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="header-template" class="hide">
|
|
||||||
<div class="form-inline webhook-header">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control key"
|
|
||||||
name="header_key[]"
|
|
||||||
placeholder="Content-Type" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control value"
|
|
||||||
name="header_value[]"
|
|
||||||
placeholder="application/json" />
|
|
||||||
<button class="btn btn-default" type="button">
|
|
||||||
<span class="icon-delete"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="reference-modal" class="modal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4>Supported Placeholders</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>
|
||||||
|
You can use the below placeholders in webhook URL, request body
|
||||||
|
and header values. {% site_name %} will replace the placeholders
|
||||||
|
with the correct values.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table id="webhook-variables" class="table modal-body">
|
||||||
|
<tr>
|
||||||
|
<th><code>$CODE</code></th>
|
||||||
|
<td>The UUID code of the check</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><code>$NAME</code></th>
|
||||||
|
<td>Name of the check</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><code>$NOW</code></th>
|
||||||
|
<td>
|
||||||
|
Current UTC time in ISO8601 format.
|
||||||
|
Example: "{{ now }}"
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><code>$STATUS</code></th>
|
||||||
|
<td>Check's current status ("up" or "down")</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><code>$TAG1, $TAG2, …</code></th>
|
||||||
|
<td>Value of the first tag, the second tag, …</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Got It!</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{% compress js %}
|
{% compress js %}
|
||||||
<script src="{% static 'js/jquery-2.1.4.min.js' %}"></script>
|
<script src="{% static 'js/jquery-2.1.4.min.js' %}"></script>
|
||||||
<script src="{% static 'js/bootstrap.min.js' %}"></script>
|
<script src="{% static 'js/bootstrap.min.js' %}"></script>
|
||||||
|
<script src="{% static 'js/bootstrap-select.min.js' %}"></script>
|
||||||
<script src="{% static 'js/webhook.js' %}"></script>
|
<script src="{% static 'js/webhook.js' %}"></script>
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
Loading…
Add table
Reference in a new issue