forked from GithubBackups/healthchecks
Variables in webhook URLs. Fixes #52
This commit is contained in:
parent
e39b714447
commit
47f2105562
20
hc/api/migrations/0026_auto_20160415_1824.py
Normal file
20
hc/api/migrations/0026_auto_20160415_1824.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9 on 2016-04-15 18:24
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('api', '0025_auto_20160216_1214'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='channel',
|
||||||
|
name='value',
|
||||||
|
field=models.TextField(blank=True),
|
||||||
|
),
|
||||||
|
]
|
@ -117,10 +117,14 @@ class Channel(models.Model):
|
|||||||
user = models.ForeignKey(User)
|
user = models.ForeignKey(User)
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
kind = models.CharField(max_length=20, choices=CHANNEL_KINDS)
|
kind = models.CharField(max_length=20, choices=CHANNEL_KINDS)
|
||||||
value = models.CharField(max_length=200, blank=True)
|
value = models.TextField(blank=True)
|
||||||
email_verified = models.BooleanField(default=False)
|
email_verified = models.BooleanField(default=False)
|
||||||
checks = models.ManyToManyField(Check)
|
checks = models.ManyToManyField(Check)
|
||||||
|
|
||||||
|
def assign_all_checks(self):
|
||||||
|
checks = Check.objects.filter(user=self.user)
|
||||||
|
self.checks.add(*checks)
|
||||||
|
|
||||||
def make_token(self):
|
def make_token(self):
|
||||||
seed = "%s%s" % (self.code, settings.SECRET_KEY)
|
seed = "%s%s" % (self.code, settings.SECRET_KEY)
|
||||||
seed = seed.encode("utf8")
|
seed = seed.encode("utf8")
|
||||||
@ -176,6 +180,18 @@ class Channel(models.Model):
|
|||||||
prio = int(prio)
|
prio = int(prio)
|
||||||
return user_key, prio, PO_PRIORITIES[prio]
|
return user_key, prio, PO_PRIORITIES[prio]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value_down(self):
|
||||||
|
assert self.kind == "webhook"
|
||||||
|
parts = self.value.split("\n")
|
||||||
|
return parts[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value_up(self):
|
||||||
|
assert self.kind == "webhook"
|
||||||
|
parts = self.value.split("\n")
|
||||||
|
return parts[1] if len(parts) == 2 else ""
|
||||||
|
|
||||||
def latest_notification(self):
|
def latest_notification(self):
|
||||||
return Notification.objects.filter(channel=self).latest()
|
return Notification.objects.filter(channel=self).latest()
|
||||||
|
|
||||||
|
@ -64,6 +64,49 @@ class NotifyTestCase(BaseTestCase):
|
|||||||
n = Notification.objects.get()
|
n = Notification.objects.get()
|
||||||
self.assertEqual(n.error, "Received status code 500")
|
self.assertEqual(n.error, "Received status code 500")
|
||||||
|
|
||||||
|
@patch("hc.api.transports.requests.request")
|
||||||
|
def test_webhooks_support_variables(self, mock_get):
|
||||||
|
template = "http://host/$CODE/$STATUS/$TAG1/$TAG2/?name=$NAME"
|
||||||
|
self._setup_data("webhook", template)
|
||||||
|
self.check.name = "Hello World"
|
||||||
|
self.check.tags = "foo bar"
|
||||||
|
self.check.save()
|
||||||
|
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
@patch("hc.api.transports.requests.request")
|
||||||
|
def test_webhooks_dollarsign_escaping(self, mock_get):
|
||||||
|
# If name or tag contains what looks like a variable reference,
|
||||||
|
# that should be left alone:
|
||||||
|
|
||||||
|
template = "http://host/$NAME"
|
||||||
|
self._setup_data("webhook", template)
|
||||||
|
self.check.name = "$TAG1"
|
||||||
|
self.check.tags = "foo"
|
||||||
|
self.check.save()
|
||||||
|
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
|
||||||
|
url = u"http://host/%24TAG1"
|
||||||
|
mock_get.assert_called_with(
|
||||||
|
"get", url, headers={"User-Agent": "healthchecks.io"}, timeout=5)
|
||||||
|
|
||||||
|
@patch("hc.api.transports.requests.request")
|
||||||
|
def test_webhook_fires_on_up_event(self, mock_get):
|
||||||
|
self._setup_data("webhook", "http://foo\nhttp://bar", status="up")
|
||||||
|
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
|
||||||
|
mock_get.assert_called_with(
|
||||||
|
"get", "http://bar", headers={"User-Agent": "healthchecks.io"},
|
||||||
|
timeout=5)
|
||||||
|
|
||||||
def test_email(self):
|
def test_email(self):
|
||||||
self._setup_data("email", "alice@example.org")
|
self._setup_data("email", "alice@example.org")
|
||||||
self.channel.notify(self.check)
|
self.channel.notify(self.check)
|
||||||
|
@ -3,6 +3,7 @@ from django.template.loader import render_to_string
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
|
from six.moves.urllib.parse import quote
|
||||||
|
|
||||||
from hc.lib import emails
|
from hc.lib import emails
|
||||||
|
|
||||||
@ -81,11 +82,33 @@ class HttpTransport(Transport):
|
|||||||
|
|
||||||
class Webhook(HttpTransport):
|
class Webhook(HttpTransport):
|
||||||
def notify(self, check):
|
def notify(self, check):
|
||||||
# Webhook integration only fires when check goes down.
|
url = self.channel.value_down
|
||||||
if check.status != "down":
|
if check.status == "up":
|
||||||
|
url = self.channel.value_up
|
||||||
|
|
||||||
|
if not url:
|
||||||
|
# If the URL is empty then we do nothing
|
||||||
return "no-op"
|
return "no-op"
|
||||||
|
|
||||||
return self.get(self.channel.value)
|
# 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))
|
||||||
|
|
||||||
|
return self.get(url)
|
||||||
|
|
||||||
def test(self):
|
def test(self):
|
||||||
return self.get(self.channel.value)
|
return self.get(self.channel.value)
|
||||||
|
@ -31,3 +31,13 @@ class AddChannelForm(forms.ModelForm):
|
|||||||
def clean_value(self):
|
def clean_value(self):
|
||||||
value = self.cleaned_data["value"]
|
value = self.cleaned_data["value"]
|
||||||
return value.strip()
|
return value.strip()
|
||||||
|
|
||||||
|
|
||||||
|
class AddWebhookForm(forms.Form):
|
||||||
|
error_css_class = "has-error"
|
||||||
|
|
||||||
|
value_down = forms.URLField(max_length=1000, required=False)
|
||||||
|
value_up = forms.URLField(max_length=1000, required=False)
|
||||||
|
|
||||||
|
def get_value(self):
|
||||||
|
return "{value_down}\n{value_up}".format(**self.cleaned_data)
|
||||||
|
@ -81,3 +81,31 @@ class AddChannelTestCase(BaseTestCase):
|
|||||||
params = "pushover_user_key=a&nonce=INVALID&prio=0"
|
params = "pushover_user_key=a&nonce=INVALID&prio=0"
|
||||||
r = self.client.get("/integrations/add_pushover/?%s" % params)
|
r = self.client.get("/integrations/add_pushover/?%s" % params)
|
||||||
assert r.status_code == 403
|
assert r.status_code == 403
|
||||||
|
|
||||||
|
def test_it_adds_two_webhook_urls_and_redirects(self):
|
||||||
|
form = {"value_down": "http://foo.com", "value_up": "https://bar.com"}
|
||||||
|
|
||||||
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
r = self.client.post("/integrations/add_webhook/", form)
|
||||||
|
self.assertRedirects(r, "/integrations/")
|
||||||
|
|
||||||
|
c = Channel.objects.get()
|
||||||
|
self.assertEqual(c.value, "http://foo.com\nhttps://bar.com")
|
||||||
|
|
||||||
|
def test_it_rejects_non_http_webhook_urls(self):
|
||||||
|
form = {"value_down": "foo", "value_up": "bar"}
|
||||||
|
|
||||||
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
r = self.client.post("/integrations/add_webhook/", form)
|
||||||
|
self.assertContains(r, "Enter a valid URL.")
|
||||||
|
|
||||||
|
self.assertEqual(Channel.objects.count(), 0)
|
||||||
|
|
||||||
|
def test_it_handles_empty_down_url(self):
|
||||||
|
form = {"value_down": "", "value_up": "http://foo.com"}
|
||||||
|
|
||||||
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
self.client.post("/integrations/add_webhook/", form)
|
||||||
|
|
||||||
|
c = Channel.objects.get()
|
||||||
|
self.assertEqual(c.value, "\nhttp://foo.com")
|
||||||
|
@ -14,7 +14,8 @@ from django.utils.six.moves.urllib.parse import urlencode
|
|||||||
from hc.accounts.models import Profile
|
from hc.accounts.models import Profile
|
||||||
from hc.api.decorators import uuid_or_400
|
from hc.api.decorators import uuid_or_400
|
||||||
from hc.api.models import Channel, Check, Ping, DEFAULT_TIMEOUT, DEFAULT_GRACE
|
from hc.api.models import Channel, Check, Ping, DEFAULT_TIMEOUT, DEFAULT_GRACE
|
||||||
from hc.front.forms import AddChannelForm, NameTagsForm, TimeoutForm
|
from hc.front.forms import (AddChannelForm, AddWebhookForm, NameTagsForm,
|
||||||
|
TimeoutForm)
|
||||||
|
|
||||||
|
|
||||||
# from itertools recipes:
|
# from itertools recipes:
|
||||||
@ -302,8 +303,7 @@ def do_add_channel(request, data):
|
|||||||
channel.user = request.user
|
channel.user = request.user
|
||||||
channel.save()
|
channel.save()
|
||||||
|
|
||||||
checks = Check.objects.filter(user=request.user)
|
channel.assign_all_checks()
|
||||||
channel.checks.add(*checks)
|
|
||||||
|
|
||||||
if channel.kind == "email":
|
if channel.kind == "email":
|
||||||
channel.send_verify_link()
|
channel.send_verify_link()
|
||||||
@ -372,7 +372,19 @@ def add_email(request):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def add_webhook(request):
|
def add_webhook(request):
|
||||||
ctx = {"page": "channels"}
|
if request.method == "POST":
|
||||||
|
form = AddWebhookForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
channel = Channel(user=request.user, kind="webhook")
|
||||||
|
channel.value = form.get_value()
|
||||||
|
channel.save()
|
||||||
|
|
||||||
|
channel.assign_all_checks()
|
||||||
|
return redirect("hc-channels")
|
||||||
|
else:
|
||||||
|
form = AddWebhookForm()
|
||||||
|
|
||||||
|
ctx = {"page": "channels", "form": form}
|
||||||
return render(request, "integrations/add_webhook.html", ctx)
|
return render(request, "integrations/add_webhook.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
@ -454,6 +466,7 @@ def add_pushover(request):
|
|||||||
}
|
}
|
||||||
return render(request, "integrations/add_pushover.html", ctx)
|
return render(request, "integrations/add_pushover.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def add_victorops(request):
|
def add_victorops(request):
|
||||||
ctx = {"page": "channels"}
|
ctx = {"page": "channels"}
|
||||||
|
@ -169,4 +169,8 @@ table.channels-table > tbody > tr > th {
|
|||||||
border: ;
|
border: ;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
border: 6px solid #EEE;
|
border: 6px solid #EEE;
|
||||||
|
}
|
||||||
|
|
||||||
|
.variable-column {
|
||||||
|
width: 160px;
|
||||||
}
|
}
|
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||||
<h4 class="update-timeout-title">Assign Checks to Channel {% if channel.kind == "po" %}{{ channel.po_value|join:" / " }}{% else %}{{ channel.value }}{% endif %}</h4>
|
<h4 class="update-timeout-title">Assign Checks to Channel</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="hidden" name="channel" value="{{ channel.code }}" />
|
<input type="hidden" name="channel" value="{{ channel.code }}" />
|
||||||
|
@ -28,23 +28,40 @@
|
|||||||
{% if ch.kind == "victorops" %} VictorOps {% endif %}
|
{% if ch.kind == "victorops" %} VictorOps {% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="value-cell">
|
<td class="value-cell">
|
||||||
<span class="preposition">
|
{% if ch.kind == "email" %}
|
||||||
{% if ch.kind == "email" %} to {% endif %}
|
<span class="preposition">to</span>
|
||||||
{% if ch.kind == "pd" %} API key {% endif %}
|
{{ ch.value }}
|
||||||
{% if ch.kind == "po" %} user key {% endif %}
|
{% if not ch.email_verified %}
|
||||||
{% if ch.kind == "victorops" %} Post URL {% endif %}
|
<span class="channel-unconfirmed">(unconfirmed)</span>
|
||||||
</span>
|
{% endif %}
|
||||||
|
{% elif ch.kind == "pd" %}
|
||||||
{% if ch.kind == "po" %}
|
<span class="preposition">API key</span>
|
||||||
|
{{ ch.value }}
|
||||||
|
{% elif ch.kind == "victorops" %}
|
||||||
|
<span class="preposition">Post URL</span>
|
||||||
|
{{ ch.value }}
|
||||||
|
{% elif ch.kind == "po" %}
|
||||||
|
<span class="preposition">user key</span>
|
||||||
{{ ch.po_value|first }}
|
{{ ch.po_value|first }}
|
||||||
({{ ch.po_value|last }} priority)
|
({{ ch.po_value|last }} priority)
|
||||||
|
{% elif ch.kind == "webhook" %}
|
||||||
|
<table>
|
||||||
|
{% if ch.value_down %}
|
||||||
|
<tr>
|
||||||
|
<td class="preposition">down </td>
|
||||||
|
<td>{{ ch.value_down }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% if ch.value_up %}
|
||||||
|
<tr>
|
||||||
|
<td class="preposition">up </td>
|
||||||
|
<td>{{ ch.value_up }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</table>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ ch.value }}
|
{{ ch.value }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if ch.kind == "email" and not ch.email_verified %}
|
|
||||||
<span class="channel-unconfirmed">(unconfirmed)
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
</td>
|
||||||
<td class="channels-num-checks">
|
<td class="channels-num-checks">
|
||||||
<a
|
<a
|
||||||
|
@ -55,7 +55,7 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="kind" value="slack" />
|
<input type="hidden" name="kind" value="slack" />
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="inputEmail3" class="col-sm-2 control-label">Callback URL</label>
|
<label class="col-sm-2 control-label">Callback URL</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<input type="text" class="form-control" name="value" placeholder="">
|
<input type="text" class="form-control" name="value" placeholder="">
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,27 +1,84 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load compress humanize staticfiles hc_extras %}
|
{% load compress humanize staticfiles hc_extras %}
|
||||||
|
|
||||||
{% block title %}Add WebHook - healthchecks.io{% endblock %}
|
{% block title %}Add Webhook - healthchecks.io{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% 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>WebHooks are a simple way to notify an external system when a check
|
<p>Webhooks are a simple way to notify an external system when a check
|
||||||
goes down. healthcheks.io will run a normal HTTP GET call to your
|
goes up or down. healthcheks.io will run a normal HTTP GET call to your
|
||||||
specified URL.</p>
|
specified URL.</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>Urlencoded name of the check</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>Urlencoded value of the first tag, the second tag, …</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<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, healthchecks.io would then call:
|
||||||
|
</p>
|
||||||
|
<pre>http://requestb.in/1hhct291?message=<strong>My%20Check</strong>:<strong>down</strong></pre>
|
||||||
|
|
||||||
<h2>Integration Settings</h2>
|
<h2>Integration Settings</h2>
|
||||||
|
|
||||||
<form method="post" class="form-horizontal" action="{% url 'hc-add-channel' %}">
|
<form method="post" class="form-horizontal">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="kind" value="webhook" />
|
<input type="hidden" name="kind" value="webhook" />
|
||||||
<div class="form-group">
|
<div class="form-group {{ form.value_down.css_classes }}">
|
||||||
<label for="inputEmail3" class="col-sm-2 control-label">URL</label>
|
<label class="col-sm-2 control-label">URL for "down" events</label>
|
||||||
<div class="col-sm-3">
|
<div class="col-sm-10">
|
||||||
<input type="text" class="form-control" name="value" placeholder="http://...">
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="value_down"
|
||||||
|
placeholder="http://..."
|
||||||
|
value="{{ form.value_down.value|default:"" }}">
|
||||||
|
{% if form.value_down.errors %}
|
||||||
|
<div class="help-block">
|
||||||
|
{{ form.value_down.errors|join:"" }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group {{ form.value_up.css_classes }}">
|
||||||
|
<label class="col-sm-2 control-label">URL for "up" events</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="value_up"
|
||||||
|
placeholder="http://..."
|
||||||
|
value="{{ form.value_up.value|default:"" }}">
|
||||||
|
{% if form.value_up.errors %}
|
||||||
|
<div class="help-block">
|
||||||
|
{{ form.value_up.errors|join:"" }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user