mirror of
https://github.com/healthchecks/healthchecks.git
synced 2025-03-15 04:34:49 +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
|
||||
- Add the `prunetokenbucket` management command
|
||||
- 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
|
||||
- Fix badges for tags containing special characters (#240, #237)
|
||||
|
|
|
@ -360,44 +360,62 @@ class Channel(models.Model):
|
|||
prio = int(parts[1])
|
||||
return PO_PRIORITIES[prio]
|
||||
|
||||
@property
|
||||
def url_down(self):
|
||||
def webhook_spec(self, status):
|
||||
assert self.kind == "webhook"
|
||||
|
||||
if not self.value.startswith("{"):
|
||||
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)
|
||||
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
|
||||
def url_up(self):
|
||||
assert self.kind == "webhook"
|
||||
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", {})
|
||||
return self.up_webhook_spec["url"]
|
||||
|
||||
@property
|
||||
def slack_team(self):
|
||||
|
|
|
@ -146,8 +146,8 @@ class NotifyTestCase(BaseTestCase):
|
|||
self.assertIsInstance(kwargs["data"], bytes)
|
||||
|
||||
@patch("hc.api.transports.requests.request")
|
||||
def test_webhooks_handle_json_value(self, mock_request):
|
||||
definition = {"url_down": "http://foo.com"}
|
||||
def test_legacy_webhooks_handle_json_value(self, mock_request):
|
||||
definition = {"url_down": "http://foo.com", "post_data": "", "headers": {}}
|
||||
self._setup_data("webhook", json.dumps(definition))
|
||||
self.channel.notify(self.check)
|
||||
|
||||
|
@ -157,8 +157,8 @@ class NotifyTestCase(BaseTestCase):
|
|||
)
|
||||
|
||||
@patch("hc.api.transports.requests.request")
|
||||
def test_webhooks_handle_json_up_event(self, mock_request):
|
||||
definition = {"url_up": "http://bar"}
|
||||
def test_legacy_webhooks_handle_json_up_event(self, mock_request):
|
||||
definition = {"url_up": "http://bar", "post_data": "", "headers": {}}
|
||||
|
||||
self._setup_data("webhook", json.dumps(definition), status="up")
|
||||
self.channel.notify(self.check)
|
||||
|
@ -167,7 +167,22 @@ class NotifyTestCase(BaseTestCase):
|
|||
mock_request.assert_called_with("get", "http://bar", headers=headers, timeout=5)
|
||||
|
||||
@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 = {
|
||||
"url_down": "http://foo.com",
|
||||
"post_data": "data",
|
||||
|
@ -183,9 +198,27 @@ class NotifyTestCase(BaseTestCase):
|
|||
)
|
||||
|
||||
@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 = {
|
||||
"url_down": "http://foo.com",
|
||||
"post_data": "",
|
||||
"headers": {"Content-Type": "application/json"},
|
||||
}
|
||||
|
||||
|
@ -198,9 +231,27 @@ class NotifyTestCase(BaseTestCase):
|
|||
)
|
||||
|
||||
@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 = {
|
||||
"url_down": "http://foo.com",
|
||||
"post_data": "",
|
||||
"headers": {"User-Agent": "My-Agent"},
|
||||
}
|
||||
|
||||
|
@ -212,11 +263,30 @@ class NotifyTestCase(BaseTestCase):
|
|||
"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")
|
||||
def test_webhooks_support_variables_in_headers(self, mock_request):
|
||||
definition = {
|
||||
"method_down": "GET",
|
||||
"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))
|
||||
|
|
|
@ -178,22 +178,24 @@ class Webhook(HttpTransport):
|
|||
return False
|
||||
|
||||
def notify(self, check):
|
||||
url = self.channel.url_down
|
||||
if check.status == "up":
|
||||
url = self.channel.url_up
|
||||
spec = self.channel.webhook_spec(check.status)
|
||||
assert spec["url"]
|
||||
|
||||
assert url
|
||||
|
||||
url = self.prepare(url, check, urlencode=True)
|
||||
url = self.prepare(spec["url"], check, urlencode=True)
|
||||
headers = {}
|
||||
for key, value in self.channel.headers.items():
|
||||
for key, value in spec["headers"].items():
|
||||
headers[key] = self.prepare(value, check)
|
||||
|
||||
if self.channel.post_data:
|
||||
payload = self.prepare(self.channel.post_data, check)
|
||||
return self.post(url, data=payload.encode(), headers=headers)
|
||||
else:
|
||||
body = spec["body"]
|
||||
if body:
|
||||
body = self.prepare(body, check)
|
||||
|
||||
if spec["method"] == "GET":
|
||||
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):
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
from datetime import timedelta as td
|
||||
import json
|
||||
import re
|
||||
from urllib.parse import quote, urlencode
|
||||
|
||||
from django import forms
|
||||
from django.forms import URLField
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator
|
||||
from hc.front.validators import (
|
||||
CronExpressionValidator,
|
||||
|
@ -14,6 +15,37 @@ from hc.front.validators import (
|
|||
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):
|
||||
name = forms.CharField(max_length=100, 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()])
|
||||
|
||||
|
||||
_valid_header_name = re.compile(r"\A[^:\s][^:\r\n]*\Z").match
|
||||
METHODS = ("GET", "POST", "PUT")
|
||||
|
||||
|
||||
class AddWebhookForm(forms.Form):
|
||||
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()]
|
||||
)
|
||||
|
||||
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(
|
||||
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):
|
||||
val = dict(self.cleaned_data)
|
||||
val["headers"] = self.headers
|
||||
return json.dumps(val, sort_keys=True)
|
||||
return json.dumps(dict(self.cleaned_data), sort_keys=True)
|
||||
|
||||
|
||||
phone_validator = RegexValidator(
|
||||
|
|
|
@ -123,3 +123,8 @@ def fix_asterisks(s):
|
|||
""" Prepend asterisks with "Combining Grapheme Joiner" characters. """
|
||||
|
||||
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):
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
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):
|
||||
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")
|
||||
r = self.client.post(self.url, form)
|
||||
self.assertRedirects(r, "/integrations/")
|
||||
|
||||
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.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):
|
||||
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
|
||||
# should work.
|
||||
|
@ -34,10 +42,8 @@ class AddWebhookTestCase(BaseTestCase):
|
|||
|
||||
c = Channel.objects.get()
|
||||
self.assertEqual(c.project, self.project)
|
||||
self.assertEqual(
|
||||
c.value,
|
||||
'{"headers": {}, "post_data": "", "url_down": "http://foo.com", "url_up": "https://bar.com"}',
|
||||
)
|
||||
self.assertEqual(c.down_webhook_spec["url"], "http://foo.com")
|
||||
self.assertEqual(c.up_webhook_spec["url"], "https://bar.com")
|
||||
|
||||
def test_it_rejects_bad_urls(self):
|
||||
urls = [
|
||||
|
@ -52,7 +58,12 @@ class AddWebhookTestCase(BaseTestCase):
|
|||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
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)
|
||||
self.assertContains(r, "Enter a valid URL.", msg_prefix=url)
|
||||
|
@ -60,35 +71,41 @@ class AddWebhookTestCase(BaseTestCase):
|
|||
self.assertEqual(Channel.objects.count(), 0)
|
||||
|
||||
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.post(self.url, form)
|
||||
|
||||
c = Channel.objects.get()
|
||||
self.assertEqual(
|
||||
c.value,
|
||||
'{"headers": {}, "post_data": "", "url_down": "", "url_up": "http://foo.com"}',
|
||||
)
|
||||
self.assertEqual(c.down_webhook_spec["url"], "")
|
||||
self.assertEqual(c.up_webhook_spec["url"], "http://foo.com")
|
||||
|
||||
def test_it_adds_post_data(self):
|
||||
form = {"url_down": "http://foo.com", "post_data": "hello"}
|
||||
def test_it_adds_request_body(self):
|
||||
form = {
|
||||
"method_down": "POST",
|
||||
"url_down": "http://foo.com",
|
||||
"body_down": "hello",
|
||||
"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.value,
|
||||
'{"headers": {}, "post_data": "hello", "url_down": "http://foo.com", "url_up": ""}',
|
||||
)
|
||||
self.assertEqual(c.down_webhook_spec["body"], "hello")
|
||||
|
||||
def test_it_adds_headers(self):
|
||||
form = {
|
||||
"method_down": "GET",
|
||||
"url_down": "http://foo.com",
|
||||
"header_key[]": ["test", "test2"],
|
||||
"header_value[]": ["123", "abc"],
|
||||
"headers_down": "test:123\ntest2:abc",
|
||||
"method_up": "GET",
|
||||
}
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
|
@ -96,16 +113,34 @@ class AddWebhookTestCase(BaseTestCase):
|
|||
self.assertRedirects(r, "/integrations/")
|
||||
|
||||
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")
|
||||
form = {
|
||||
"method_down": "GET",
|
||||
"url_down": "http://example.org",
|
||||
"header_key[]": ["ill:egal"],
|
||||
"header_value[]": ["123"],
|
||||
"headers_down": "invalid-headers",
|
||||
"method_up": "GET",
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
#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 haveBlankHeaderForm() {
|
||||
return $("#webhook-headers .webhook-header").filter(function() {
|
||||
var key = $(".key", this).val();
|
||||
var value = $(".value", this).val();
|
||||
return !key && !value;
|
||||
}).length;
|
||||
}
|
||||
$("#method-down").change(function() {
|
||||
var method = this.value;
|
||||
$("#body-down-group").toggle(method != "GET");
|
||||
});
|
||||
|
||||
function ensureBlankHeaderForm() {
|
||||
if (!haveBlankHeaderForm()) {
|
||||
var tmpl = $("#header-template").html();
|
||||
$("#webhook-headers").append(tmpl);
|
||||
}
|
||||
}
|
||||
$("#method-up").change(function() {
|
||||
var method = this.value;
|
||||
$("#body-up-group").toggle(method != "GET");
|
||||
});
|
||||
|
||||
$("#webhook-headers").on("click", "button", function(e) {
|
||||
e.preventDefault();
|
||||
$(this).closest(".webhook-header").remove();
|
||||
ensureBlankHeaderForm();
|
||||
})
|
||||
|
||||
$("#webhook-headers").on("keyup", "input", ensureBlankHeaderForm);
|
||||
ensureBlankHeaderForm();
|
||||
// On page load, check if we need to show "request body" fields
|
||||
$("#method-down").trigger("change");
|
||||
$("#method-up").trigger("change");
|
||||
});
|
|
@ -398,30 +398,38 @@
|
|||
</div>
|
||||
|
||||
{% 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 %}
|
||||
<p><strong>URL for "down" events</strong></p>
|
||||
<pre>{{ ch.url_down }}</pre>
|
||||
{% if spec.headers %}
|
||||
<p>Request Headers</p>
|
||||
<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 %}
|
||||
|
||||
{% 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 class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
|
|
|
@ -6,168 +6,201 @@
|
|||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<h1>Webhook</h1>
|
||||
<div class="col-sm-12">
|
||||
<h1>Webhook</h1>
|
||||
|
||||
<p>Runs a HTTP GET or HTTP POST to your specified URL when a check
|
||||
goes up or down. Uses GET by default, and uses POST if you specify
|
||||
any POST data.</p>
|
||||
<p>Executes an HTTP request to your specified URL when a check
|
||||
goes up or down.</p>
|
||||
|
||||
<p>You can use the following variables in webhook URLs:</p>
|
||||
<table class="table webhook-variables">
|
||||
<tr>
|
||||
<th class="variable-column">Variable</th>
|
||||
<td>Will be replaced with…</td>
|
||||
</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>
|
||||
You can use placeholders <strong>$NAME</strong>, <strong>$STATUS</strong> and others in webhook URLs,
|
||||
request body and header values
|
||||
<a href="#" data-toggle="modal" data-target="#reference-modal">(quick reference)</a>.
|
||||
</p>
|
||||
|
||||
<p>For example, a callback URL using variables might look like so:
|
||||
<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">
|
||||
<form id="add-webhook-form" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="kind" value="webhook" />
|
||||
<div class="form-group {{ form.url_down.css_classes }}">
|
||||
<label class="col-sm-2 control-label">URL for "down" events</label>
|
||||
<div class="col-sm-10">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="url_down"
|
||||
placeholder="http://..."
|
||||
value="{{ form.url_down.value|default:"" }}">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<h2>Execute when a check goes <strong class="label-down">down</strong></h2>
|
||||
<br />
|
||||
|
||||
<div class="form-group {{ form.url_down.css_classes }}">
|
||||
<label>URL</label>
|
||||
<div class="method-url-group">
|
||||
<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 %}
|
||||
<div class="help-block">
|
||||
{{ form.url_down.errors|join:"" }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group {{ form.url_up.css_classes }}">
|
||||
<label class="col-sm-2 control-label">URL for "up" events</label>
|
||||
<div class="col-sm-10">
|
||||
<input
|
||||
type="text"
|
||||
|
||||
<div id="body-down-group" class="form-group {{ form.body_down.css_classes }}" style="display: none">
|
||||
<label class="control-label">Request Body</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
name="url_up"
|
||||
placeholder="http://..."
|
||||
value="{{ form.url_up.value|default:"" }}">
|
||||
rows="3"
|
||||
name="body_down"
|
||||
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 %}
|
||||
<div class="help-block">
|
||||
{{ form.url_up.errors|join:"" }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group {{ form.post_data.css_classes }}">
|
||||
<label class="col-sm-2 control-label">POST data</label>
|
||||
<div class="col-sm-10">
|
||||
<input
|
||||
type="text"
|
||||
<div id="body-up-group" class="form-group {{ form.body_up.css_classes }}" style="display: none">
|
||||
<label class="control-label">Request Body</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
name="post_data"
|
||||
placeholder='{"status": "$STATUS"}'
|
||||
value="{{ form.post_data.value|default:"" }}">
|
||||
rows="3"
|
||||
name="body_up"
|
||||
placeholder='{"status": "$STATUS"}'>{{ form.body_up.value|default:"" }}</textarea>
|
||||
{% if form.post_data.errors %}
|
||||
<div class="help-block">
|
||||
{{ form.post_data.errors|join:"" }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">Request Headers</label>
|
||||
<div class="col-xs-12 col-sm-10">
|
||||
<div id="webhook-headers">
|
||||
{% for k, v in form.headers.items %}
|
||||
<div class="form-inline webhook-header">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control key {% if k in form.invalid_header_names %}error{% endif %}"
|
||||
name="header_key[]"
|
||||
placeholder="Content-Type"
|
||||
value="{{ k }}" />
|
||||
<input
|
||||
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 class="form-group {{ form.headers_up.css_classes }}">
|
||||
<label class="control-label">Request Headers</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
rows="3"
|
||||
name="headers_up"
|
||||
placeholder="X-Sample-Header: $NAME is back up">{{ form.headers_up.value|default:"" }}</textarea>
|
||||
<div class="help-block">
|
||||
{% if form.headers_up.errors %}
|
||||
{{ form.headers_up.errors|join:"" }}
|
||||
{% else %}
|
||||
Optional "Header-Name: value" pairs, one pair per line.
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if form.invalid_header_names %}
|
||||
<div class="text-danger">
|
||||
Please use valid HTTP header names.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
</div>
|
||||
|
||||
<div class="form-group" class="clearfix">
|
||||
<br>
|
||||
<br>
|
||||
<div class="text-right">
|
||||
<button type="submit" class="btn btn-primary">Save Integration</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<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 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 %}
|
||||
|
||||
{% block scripts %}
|
||||
{% compress js %}
|
||||
<script src="{% static 'js/jquery-2.1.4.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>
|
||||
{% endcompress %}
|
||||
{% endblock %}
|
||||
|
|
Loading…
Reference in a new issue