forked from GithubBackups/healthchecks
Webhooks support POST, cleanup.
This commit is contained in:
parent
891efc720a
commit
c16eeda004
@ -269,9 +269,6 @@ class Channel(models.Model):
|
||||
|
||||
return error
|
||||
|
||||
def test(self):
|
||||
return self.transport().test()
|
||||
|
||||
@property
|
||||
def po_value(self):
|
||||
assert self.kind == "po"
|
||||
@ -289,7 +286,13 @@ class Channel(models.Model):
|
||||
def value_up(self):
|
||||
assert self.kind == "webhook"
|
||||
parts = self.value.split("\n")
|
||||
return parts[1] if len(parts) == 2 else ""
|
||||
return parts[1] if len(parts) > 1 else ""
|
||||
|
||||
@property
|
||||
def post_data(self):
|
||||
assert self.kind == "webhook"
|
||||
parts = self.value.split("\n")
|
||||
return parts[2] if len(parts) > 2 else ""
|
||||
|
||||
@property
|
||||
def slack_team(self):
|
||||
@ -321,18 +324,12 @@ class Channel(models.Model):
|
||||
@property
|
||||
def discord_webhook_url(self):
|
||||
assert self.kind == "discord"
|
||||
if not self.value.startswith("{"):
|
||||
return self.value
|
||||
|
||||
doc = json.loads(self.value)
|
||||
return doc["webhook"]["url"]
|
||||
|
||||
@property
|
||||
def discord_webhook_id(self):
|
||||
assert self.kind == "discord"
|
||||
if not self.value.startswith("{"):
|
||||
return self.value
|
||||
|
||||
doc = json.loads(self.value)
|
||||
return doc["webhook"]["id"]
|
||||
|
||||
|
@ -80,8 +80,25 @@ class NotifyTestCase(BaseTestCase):
|
||||
url = u"http://host/%s/down/foo/bar/?name=Hello%%20World" \
|
||||
% self.check.code
|
||||
|
||||
mock_get.assert_called_with(
|
||||
"get", url, headers={"User-Agent": "healthchecks.io"}, timeout=5)
|
||||
args, kwargs = mock_get.call_args
|
||||
self.assertEqual(args[0], "get")
|
||||
self.assertEqual(args[1], url)
|
||||
self.assertEqual(kwargs["headers"], {"User-Agent": "healthchecks.io"})
|
||||
self.assertEqual(kwargs["timeout"], 5)
|
||||
|
||||
@patch("hc.api.transports.requests.request")
|
||||
def test_webhooks_support_post(self, mock_request):
|
||||
template = "http://example.com\n\nThe Time Is $NOW"
|
||||
self._setup_data("webhook", template)
|
||||
self.check.save()
|
||||
|
||||
self.channel.notify(self.check)
|
||||
args, kwargs = mock_request.call_args
|
||||
self.assertEqual(args[0], "post")
|
||||
self.assertEqual(args[1], "http://example.com")
|
||||
|
||||
# spaces should not have been urlencoded:
|
||||
self.assertTrue(kwargs["data"].startswith("The Time Is 2"))
|
||||
|
||||
@patch("hc.api.transports.requests.request")
|
||||
def test_webhooks_dollarsign_escaping(self, mock_get):
|
||||
@ -267,3 +284,15 @@ class NotifyTestCase(BaseTestCase):
|
||||
attachment = payload["attachments"][0]
|
||||
fields = {f["title"]: f["value"] for f in attachment["fields"]}
|
||||
self.assertEqual(fields["Last Ping"], "Never")
|
||||
|
||||
@patch("hc.api.transports.requests.request")
|
||||
def test_pushbullet(self, mock_post):
|
||||
self._setup_data("pushbullet", "fake-token")
|
||||
mock_post.return_value.status_code = 200
|
||||
|
||||
self.channel.notify(self.check)
|
||||
assert Notification.objects.count() == 1
|
||||
|
||||
_, kwargs = mock_post.call_args
|
||||
self.assertEqual(kwargs["json"]["type"], "note")
|
||||
self.assertEqual(kwargs["headers"]["Access-Token"], "fake-token")
|
||||
|
@ -83,14 +83,43 @@ class HttpTransport(Transport):
|
||||
def get(self, url):
|
||||
return self.request("get", url)
|
||||
|
||||
def post(self, url, json, **kwargs):
|
||||
return self.request("post", url, json=json, **kwargs)
|
||||
|
||||
def post_form(self, url, data):
|
||||
return self.request("post", url, data=data)
|
||||
def post(self, url, **kwargs):
|
||||
return self.request("post", url, **kwargs)
|
||||
|
||||
|
||||
class Webhook(HttpTransport):
|
||||
def prepare(self, template, check, urlencode=False):
|
||||
""" Replace variables with actual values.
|
||||
|
||||
There should be no bad translations if users use $ symbol in
|
||||
check's name or tags, because $ gets urlencoded to %24
|
||||
|
||||
"""
|
||||
|
||||
def safe(s):
|
||||
return quote(s) if urlencode else s
|
||||
|
||||
result = template
|
||||
if "$CODE" in result:
|
||||
result = result.replace("$CODE", str(check.code))
|
||||
|
||||
if "$STATUS" in result:
|
||||
result = result.replace("$STATUS", check.status)
|
||||
|
||||
if "$NOW" in result:
|
||||
s = timezone.now().replace(microsecond=0).isoformat()
|
||||
result = result.replace("$NOW", safe(s))
|
||||
|
||||
if "$NAME" in result:
|
||||
result = result.replace("$NAME", safe(check.name))
|
||||
|
||||
if "$TAG" in result:
|
||||
for i, tag in enumerate(check.tags_list()):
|
||||
placeholder = "$TAG%d" % (i + 1)
|
||||
result = result.replace(placeholder, safe(tag))
|
||||
|
||||
return result
|
||||
|
||||
def notify(self, check):
|
||||
url = self.channel.value_down
|
||||
if check.status == "up":
|
||||
@ -100,35 +129,19 @@ class Webhook(HttpTransport):
|
||||
# If the URL is empty then we do nothing
|
||||
return "no-op"
|
||||
|
||||
# Replace variables with actual values.
|
||||
# There should be no bad translations if users use $ symbol in
|
||||
# check's name or tags, because $ gets urlencoded to %24
|
||||
|
||||
if "$CODE" in url:
|
||||
url = url.replace("$CODE", str(check.code))
|
||||
|
||||
if "$STATUS" in url:
|
||||
url = url.replace("$STATUS", check.status)
|
||||
|
||||
if "$NAME" in url:
|
||||
url = url.replace("$NAME", quote(check.name))
|
||||
|
||||
if "$TAG" in url:
|
||||
for i, tag in enumerate(check.tags_list()):
|
||||
placeholder = "$TAG%d" % (i + 1)
|
||||
url = url.replace(placeholder, quote(tag))
|
||||
|
||||
url = self.prepare(url, check, urlencode=True)
|
||||
if self.channel.post_data:
|
||||
payload = self.prepare(self.channel.post_data, check)
|
||||
return self.post(url, data=payload)
|
||||
else:
|
||||
return self.get(url)
|
||||
|
||||
def test(self):
|
||||
return self.get(self.channel.value)
|
||||
|
||||
|
||||
class Slack(HttpTransport):
|
||||
def notify(self, check):
|
||||
text = tmpl("slack_message.json", check=check)
|
||||
payload = json.loads(text)
|
||||
return self.post(self.channel.slack_webhook_url, payload)
|
||||
return self.post(self.channel.slack_webhook_url, json=payload)
|
||||
|
||||
|
||||
class HipChat(HttpTransport):
|
||||
@ -138,7 +151,7 @@ class HipChat(HttpTransport):
|
||||
"message": text,
|
||||
"color": "green" if check.status == "up" else "red",
|
||||
}
|
||||
return self.post(self.channel.value, payload)
|
||||
return self.post(self.channel.value, json=payload)
|
||||
|
||||
|
||||
class OpsGenie(HttpTransport):
|
||||
@ -159,7 +172,7 @@ class OpsGenie(HttpTransport):
|
||||
if check.status == "up":
|
||||
url += "/close"
|
||||
|
||||
return self.post(url, payload)
|
||||
return self.post(url, json=payload)
|
||||
|
||||
|
||||
class PagerDuty(HttpTransport):
|
||||
@ -176,7 +189,7 @@ class PagerDuty(HttpTransport):
|
||||
"client_url": settings.SITE_ROOT
|
||||
}
|
||||
|
||||
return self.post(self.URL, payload)
|
||||
return self.post(self.URL, json=payload)
|
||||
|
||||
|
||||
class Pushbullet(HttpTransport):
|
||||
@ -193,7 +206,7 @@ class Pushbullet(HttpTransport):
|
||||
"body": text
|
||||
}
|
||||
|
||||
return self.post(url, payload, headers=headers)
|
||||
return self.post(url, json=payload, headers=headers)
|
||||
|
||||
|
||||
class Pushover(HttpTransport):
|
||||
@ -222,7 +235,7 @@ class Pushover(HttpTransport):
|
||||
payload["retry"] = settings.PUSHOVER_EMERGENCY_RETRY_DELAY
|
||||
payload["expire"] = settings.PUSHOVER_EMERGENCY_EXPIRATION
|
||||
|
||||
return self.post_form(self.URL, payload)
|
||||
return self.post(self.URL, data=payload)
|
||||
|
||||
|
||||
class VictorOps(HttpTransport):
|
||||
@ -236,11 +249,12 @@ class VictorOps(HttpTransport):
|
||||
"monitoring_tool": "healthchecks.io",
|
||||
}
|
||||
|
||||
return self.post(self.channel.value, payload)
|
||||
return self.post(self.channel.value, json=payload)
|
||||
|
||||
|
||||
class Discord(HttpTransport):
|
||||
def notify(self, check):
|
||||
text = tmpl("slack_message.json", check=check)
|
||||
payload = json.loads(text)
|
||||
return self.post(self.channel.discord_webhook_url + "/slack", payload)
|
||||
url = self.channel.discord_webhook_url + "/slack"
|
||||
return self.post(url, json=payload)
|
||||
|
@ -60,5 +60,8 @@ class AddWebhookForm(forms.Form):
|
||||
value_up = forms.URLField(max_length=1000, required=False,
|
||||
validators=[WebhookValidator()])
|
||||
|
||||
post_data = forms.CharField(max_length=1000, required=False)
|
||||
|
||||
def get_value(self):
|
||||
return "{value_down}\n{value_up}".format(**self.cleaned_data)
|
||||
d = self.cleaned_data
|
||||
return "\n".join((d["value_down"], d["value_up"], d["post_data"]))
|
||||
|
@ -8,7 +8,7 @@ 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, "Webhooks are a simple way")
|
||||
self.assertContains(r, "Runs a HTTP GET or HTTP POST")
|
||||
|
||||
def test_it_adds_two_webhook_urls_and_redirects(self):
|
||||
form = {"value_down": "http://foo.com", "value_up": "https://bar.com"}
|
||||
@ -18,7 +18,7 @@ class AddWebhookTestCase(BaseTestCase):
|
||||
self.assertRedirects(r, "/integrations/")
|
||||
|
||||
c = Channel.objects.get()
|
||||
self.assertEqual(c.value, "http://foo.com\nhttps://bar.com")
|
||||
self.assertEqual(c.value, "http://foo.com\nhttps://bar.com\n")
|
||||
|
||||
def test_it_adds_webhook_using_team_access(self):
|
||||
form = {"value_down": "http://foo.com", "value_up": "https://bar.com"}
|
||||
@ -30,7 +30,7 @@ class AddWebhookTestCase(BaseTestCase):
|
||||
|
||||
c = Channel.objects.get()
|
||||
self.assertEqual(c.user, self.alice)
|
||||
self.assertEqual(c.value, "http://foo.com\nhttps://bar.com")
|
||||
self.assertEqual(c.value, "http://foo.com\nhttps://bar.com\n")
|
||||
|
||||
def test_it_rejects_bad_urls(self):
|
||||
urls = [
|
||||
@ -59,4 +59,14 @@ class AddWebhookTestCase(BaseTestCase):
|
||||
self.client.post(self.url, form)
|
||||
|
||||
c = Channel.objects.get()
|
||||
self.assertEqual(c.value, "\nhttp://foo.com")
|
||||
self.assertEqual(c.value, "\nhttp://foo.com\n")
|
||||
|
||||
def test_it_adds_post_data(self):
|
||||
form = {"value_down": "http://foo.com", "post_data": "hello"}
|
||||
|
||||
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, "http://foo.com\n\nhello")
|
||||
|
@ -22,3 +22,28 @@ class ChannelsTestCase(BaseTestCase):
|
||||
r = self.client.get("/integrations/")
|
||||
self.assertContains(r, "foo-team", status_code=200)
|
||||
self.assertContains(r, "#bar")
|
||||
|
||||
def test_it_shows_webhook_post_data(self):
|
||||
ch = Channel(kind="webhook", user=self.alice)
|
||||
ch.value = "http://down.example.com\nhttp://up.example.com\nfoobar"
|
||||
ch.save()
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.get("/integrations/")
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertContains(r, "<td>http://down.example.com</td>")
|
||||
self.assertContains(r, "<td>http://up.example.com</td>")
|
||||
self.assertContains(r, "<td>foobar</td>")
|
||||
|
||||
def test_it_shows_pushover_details(self):
|
||||
ch = Channel(kind="po", user=self.alice)
|
||||
ch.value = "fake-key|0"
|
||||
ch.save()
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.get("/integrations/")
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertContains(r, "fake-key")
|
||||
self.assertContains(r, "(normal priority)")
|
||||
|
@ -410,7 +410,11 @@ def add_webhook(request):
|
||||
else:
|
||||
form = AddWebhookForm()
|
||||
|
||||
ctx = {"page": "channels", "form": form}
|
||||
ctx = {
|
||||
"page": "channels",
|
||||
"form": form,
|
||||
"now": timezone.now().replace(microsecond=0).isoformat()
|
||||
}
|
||||
return render(request, "integrations/add_webhook.html", ctx)
|
||||
|
||||
|
||||
|
@ -81,6 +81,12 @@
|
||||
<td>{{ ch.value_up }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if ch.post_data %}
|
||||
<tr>
|
||||
<td class="preposition">body </td>
|
||||
<td>{{ ch.post_data }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
{% elif ch.kind == "pushbullet" %}
|
||||
<span class="preposition">API key</span>
|
||||
|
@ -9,9 +9,10 @@
|
||||
<div class="col-sm-12">
|
||||
<h1>Webhook</h1>
|
||||
|
||||
<p>Webhooks are a simple way to notify an external system when a check
|
||||
goes up or down. healthcheks.io will run a normal HTTP GET call to your
|
||||
specified URL.</p>
|
||||
<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>You can use the following variables in webhook URLs:</p>
|
||||
<table class="table webhook-variables">
|
||||
<tr>
|
||||
@ -24,7 +25,14 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th><code>$NAME</code></th>
|
||||
<td>Urlencoded name of the check</td>
|
||||
<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>
|
||||
@ -32,7 +40,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th><code>$TAG1, $TAG2, …</code></th>
|
||||
<td>Urlencoded value of the first tag, the second tag, …</td>
|
||||
<td>Value of the first tag, the second tag, …</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@ -81,6 +89,22 @@
|
||||
{% 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"
|
||||
class="form-control"
|
||||
name="post_data"
|
||||
placeholder='{"status": "$STATUS"}'
|
||||
value="{{ form.post_data.value|default:"" }}">
|
||||
{% if form.post_data.errors %}
|
||||
<div class="help-block">
|
||||
{{ form.post_data.errors|join:"" }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button type="submit" class="btn btn-primary">Save Integration</button>
|
||||
|
Loading…
x
Reference in New Issue
Block a user