forked from GithubBackups/healthchecks
parent
dd45c888a7
commit
524d1a7375
@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Add retries to the the email sending logic
|
- Add retries to the the email sending logic
|
||||||
- Require confirmation codes (sent to email) before sensitive actions
|
- Require confirmation codes (sent to email) before sensitive actions
|
||||||
- Implement WebAuthn two-factor authentication
|
- Implement WebAuthn two-factor authentication
|
||||||
|
- Implement badge mode (up/down vs up/late/down) selector (#282)
|
||||||
|
|
||||||
## v1.17.0 - 2020-10-14
|
## v1.17.0 - 2020-10-14
|
||||||
|
|
||||||
|
@ -15,12 +15,15 @@ class BadgeTestCase(BaseTestCase):
|
|||||||
|
|
||||||
sig = base64_hmac(str(self.project.badge_key), "foo", settings.SECRET_KEY)
|
sig = base64_hmac(str(self.project.badge_key), "foo", settings.SECRET_KEY)
|
||||||
sig = sig[:8]
|
sig = sig[:8]
|
||||||
self.svg_url = "/badge/%s/%s/foo.svg" % (self.project.badge_key, sig)
|
|
||||||
self.json_url = "/badge/%s/%s/foo.json" % (self.project.badge_key, sig)
|
self.svg_url = "/badge/%s/%s-2/foo.svg" % (self.project.badge_key, sig)
|
||||||
|
self.json_url = "/badge/%s/%s-2/foo.json" % (self.project.badge_key, sig)
|
||||||
|
self.with_late_url = "/badge/%s/%s/foo.json" % (self.project.badge_key, sig)
|
||||||
|
self.shields_url = "/badge/%s/%s-2/foo.shields" % (self.project.badge_key, sig)
|
||||||
|
|
||||||
def test_it_rejects_bad_signature(self):
|
def test_it_rejects_bad_signature(self):
|
||||||
r = self.client.get("/badge/%s/12345678/foo.svg" % self.project.badge_key)
|
r = self.client.get("/badge/%s/12345678/foo.svg" % self.project.badge_key)
|
||||||
assert r.status_code == 404
|
self.assertEqual(r.status_code, 404)
|
||||||
|
|
||||||
def test_it_returns_svg(self):
|
def test_it_returns_svg(self):
|
||||||
r = self.client.get(self.svg_url)
|
r = self.client.get(self.svg_url)
|
||||||
@ -37,52 +40,24 @@ class BadgeTestCase(BaseTestCase):
|
|||||||
self.assertEqual(r["Access-Control-Allow-Origin"], "*")
|
self.assertEqual(r["Access-Control-Allow-Origin"], "*")
|
||||||
|
|
||||||
def test_it_handles_new(self):
|
def test_it_handles_new(self):
|
||||||
r = self.client.get(self.json_url)
|
doc = self.client.get(self.json_url).json()
|
||||||
doc = r.json()
|
self.assertEqual(doc, {"status": "up", "total": 1, "grace": 0, "down": 0})
|
||||||
self.assertEqual(doc["status"], "up")
|
|
||||||
self.assertEqual(doc["total"], 1)
|
|
||||||
self.assertEqual(doc["grace"], 0)
|
|
||||||
self.assertEqual(doc["down"], 0)
|
|
||||||
|
|
||||||
def test_it_handles_started_but_down(self):
|
def test_it_ignores_started_when_down(self):
|
||||||
self.check.last_start = now()
|
self.check.last_start = now()
|
||||||
self.check.tags = "foo"
|
|
||||||
self.check.status = "down"
|
self.check.status = "down"
|
||||||
self.check.save()
|
self.check.save()
|
||||||
|
|
||||||
r = self.client.get(self.json_url)
|
doc = self.client.get(self.json_url).json()
|
||||||
doc = r.json()
|
self.assertEqual(doc, {"status": "down", "total": 1, "grace": 0, "down": 1})
|
||||||
self.assertEqual(doc["status"], "down")
|
|
||||||
self.assertEqual(doc["total"], 1)
|
|
||||||
self.assertEqual(doc["grace"], 0)
|
|
||||||
self.assertEqual(doc["down"], 1)
|
|
||||||
|
|
||||||
def test_it_shows_grace_badge(self):
|
def test_it_treats_late_as_up(self):
|
||||||
self.check.last_ping = now() - td(days=1, minutes=10)
|
self.check.last_ping = now() - td(days=1, minutes=10)
|
||||||
self.check.tags = "foo"
|
|
||||||
self.check.status = "up"
|
self.check.status = "up"
|
||||||
self.check.save()
|
self.check.save()
|
||||||
|
|
||||||
r = self.client.get(self.json_url)
|
doc = self.client.get(self.json_url).json()
|
||||||
doc = r.json()
|
self.assertEqual(doc, {"status": "up", "total": 1, "grace": 1, "down": 0})
|
||||||
self.assertEqual(doc["status"], "late")
|
|
||||||
self.assertEqual(doc["total"], 1)
|
|
||||||
self.assertEqual(doc["grace"], 1)
|
|
||||||
self.assertEqual(doc["down"], 0)
|
|
||||||
|
|
||||||
def test_it_shows_started_but_grace_badge(self):
|
|
||||||
self.check.last_start = now()
|
|
||||||
self.check.last_ping = now() - td(days=1, minutes=10)
|
|
||||||
self.check.tags = "foo"
|
|
||||||
self.check.status = "up"
|
|
||||||
self.check.save()
|
|
||||||
|
|
||||||
r = self.client.get(self.json_url)
|
|
||||||
doc = r.json()
|
|
||||||
self.assertEqual(doc["status"], "late")
|
|
||||||
self.assertEqual(doc["total"], 1)
|
|
||||||
self.assertEqual(doc["grace"], 1)
|
|
||||||
self.assertEqual(doc["down"], 0)
|
|
||||||
|
|
||||||
def test_it_handles_special_characters(self):
|
def test_it_handles_special_characters(self):
|
||||||
self.check.tags = "db@dc1"
|
self.check.tags = "db@dc1"
|
||||||
@ -94,3 +69,24 @@ class BadgeTestCase(BaseTestCase):
|
|||||||
|
|
||||||
r = self.client.get(url)
|
r = self.client.get(url)
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
|
def test_late_mode_returns_late_status(self):
|
||||||
|
self.check.last_ping = now() - td(days=1, minutes=10)
|
||||||
|
self.check.status = "up"
|
||||||
|
self.check.save()
|
||||||
|
|
||||||
|
doc = self.client.get(self.with_late_url).json()
|
||||||
|
self.assertEqual(doc, {"status": "late", "total": 1, "grace": 1, "down": 0})
|
||||||
|
|
||||||
|
def test_late_mode_ignores_started_when_late(self):
|
||||||
|
self.check.last_start = now()
|
||||||
|
self.check.last_ping = now() - td(days=1, minutes=10)
|
||||||
|
self.check.status = "up"
|
||||||
|
self.check.save()
|
||||||
|
|
||||||
|
doc = self.client.get(self.with_late_url).json()
|
||||||
|
self.assertEqual(doc, {"status": "late", "total": 1, "grace": 1, "down": 0})
|
||||||
|
|
||||||
|
def test_it_returns_shields_json(self):
|
||||||
|
doc = self.client.get(self.shields_url).json()
|
||||||
|
self.assertEqual(doc, {"label": "foo", "message": "up", "color": "success"})
|
||||||
|
@ -375,11 +375,15 @@ def flips_by_unique_key(request, unique_key):
|
|||||||
|
|
||||||
@never_cache
|
@never_cache
|
||||||
@cors("GET")
|
@cors("GET")
|
||||||
def badge(request, badge_key, signature, tag, fmt="svg"):
|
def badge(request, badge_key, signature, tag, fmt):
|
||||||
if not check_signature(badge_key, tag, signature):
|
if fmt not in ("svg", "json", "shields"):
|
||||||
return HttpResponseNotFound()
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
if fmt not in ("svg", "json", "shields"):
|
with_late = True
|
||||||
|
if len(signature) == 10 and signature.endswith("-2"):
|
||||||
|
with_late = False
|
||||||
|
|
||||||
|
if not check_signature(badge_key, tag, signature):
|
||||||
return HttpResponseNotFound()
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
q = Check.objects.filter(project__badge_key=badge_key)
|
q = Check.objects.filter(project__badge_key=badge_key)
|
||||||
@ -406,7 +410,7 @@ def badge(request, badge_key, signature, tag, fmt="svg"):
|
|||||||
break
|
break
|
||||||
elif check_status == "grace":
|
elif check_status == "grace":
|
||||||
grace += 1
|
grace += 1
|
||||||
if status == "up":
|
if status == "up" and with_late:
|
||||||
status = "late"
|
status = "late"
|
||||||
|
|
||||||
if fmt == "shields":
|
if fmt == "shields":
|
||||||
|
@ -676,14 +676,18 @@ def badges(request, code):
|
|||||||
sorted_tags = sorted(tags, key=lambda s: s.lower())
|
sorted_tags = sorted(tags, key=lambda s: s.lower())
|
||||||
sorted_tags.append("*") # For the "overall status" badge
|
sorted_tags.append("*") # For the "overall status" badge
|
||||||
|
|
||||||
|
key = project.badge_key
|
||||||
urls = []
|
urls = []
|
||||||
for tag in sorted_tags:
|
for tag in sorted_tags:
|
||||||
urls.append(
|
urls.append(
|
||||||
{
|
{
|
||||||
"tag": tag,
|
"tag": tag,
|
||||||
"svg": get_badge_url(project.badge_key, tag),
|
"svg": get_badge_url(key, tag),
|
||||||
"json": get_badge_url(project.badge_key, tag, fmt="json"),
|
"svg3": get_badge_url(key, tag, with_late=True),
|
||||||
"shields": get_badge_url(project.badge_key, tag, fmt="shields"),
|
"json": get_badge_url(key, tag, fmt="json"),
|
||||||
|
"json3": get_badge_url(key, tag, fmt="json", with_late=True),
|
||||||
|
"shields": get_badge_url(key, tag, fmt="shields"),
|
||||||
|
"shields3": get_badge_url(key, tag, fmt="shields", with_late=True),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -99,16 +99,17 @@ def get_badge_svg(tag, status):
|
|||||||
|
|
||||||
def check_signature(username, tag, sig):
|
def check_signature(username, tag, sig):
|
||||||
ours = base64_hmac(str(username), tag, settings.SECRET_KEY)
|
ours = base64_hmac(str(username), tag, settings.SECRET_KEY)
|
||||||
ours = ours[:8]
|
return ours[:8] == sig[:8]
|
||||||
return ours == sig
|
|
||||||
|
|
||||||
|
|
||||||
def get_badge_url(username, tag, fmt="svg"):
|
def get_badge_url(username, tag, fmt="svg", with_late=False):
|
||||||
sig = base64_hmac(str(username), tag, settings.SECRET_KEY)
|
sig = base64_hmac(str(username), tag, settings.SECRET_KEY)[:8]
|
||||||
|
if not with_late:
|
||||||
|
sig += "-2"
|
||||||
|
|
||||||
if tag == "*":
|
if tag == "*":
|
||||||
url = reverse("hc-badge-all", args=[username, sig[:8], fmt])
|
url = reverse("hc-badge-all", args=[username, sig, fmt])
|
||||||
else:
|
else:
|
||||||
url = reverse("hc-badge", args=[username, sig[:8], tag, fmt])
|
url = reverse("hc-badge", args=[username, sig, tag, fmt])
|
||||||
|
|
||||||
return settings.SITE_ROOT + url
|
return settings.SITE_ROOT + url
|
||||||
|
16
static/css/badges.css
Normal file
16
static/css/badges.css
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
.table.badge-preview th {
|
||||||
|
border-top: 0;
|
||||||
|
color: #777777;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 12px;
|
||||||
|
padding-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#badges-json .fetch-json {
|
||||||
|
background: #eee;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#badges-json, #badges-shields, .badge-preview .with-late {
|
||||||
|
display: none;
|
||||||
|
}
|
@ -25,28 +25,6 @@
|
|||||||
background-color: #ffebea;
|
background-color: #ffebea;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table.badges th {
|
|
||||||
border-top: 0;
|
|
||||||
color: #777777;
|
|
||||||
font-weight: normal;
|
|
||||||
font-size: 12px;
|
|
||||||
padding-top: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#badges-json, #badges-shields {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#badges-shields label:first-child {
|
|
||||||
margin: 20px 0 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-response code {
|
|
||||||
display: inline-block;
|
|
||||||
background: #eee;
|
|
||||||
padding: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.invite-suggestion {
|
.invite-suggestion {
|
||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
$(function() {
|
$(function() {
|
||||||
|
|
||||||
$(".json-response").each(function(idx, el) {
|
$(".fetch-json").each(function(idx, el) {
|
||||||
$.getJSON(el.dataset.url, function(data) {
|
$.getJSON(el.dataset.url, function(data) {
|
||||||
el.innerHTML = "<code>" + JSON.stringify(data) + "</code>";
|
el.innerText = JSON.stringify(data);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -23,4 +23,15 @@ $(function() {
|
|||||||
$("#badges-json").hide();
|
$("#badges-json").hide();
|
||||||
$("#badges-shields").show();
|
$("#badges-shields").show();
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$("#show-with-late").click(function() {
|
||||||
|
$(".no-late").hide();
|
||||||
|
$(".with-late").show();
|
||||||
|
})
|
||||||
|
|
||||||
|
$("#show-no-late").click(function() {
|
||||||
|
$(".with-late").hide();
|
||||||
|
$(".no-late").show();
|
||||||
|
})
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
<link rel="stylesheet" href="{% static 'css/add_project_modal.css' %}" type="text/css">
|
<link rel="stylesheet" href="{% static 'css/add_project_modal.css' %}" type="text/css">
|
||||||
<link rel="stylesheet" href="{% static 'css/add_pushover.css' %}" type="text/css">
|
<link rel="stylesheet" href="{% static 'css/add_pushover.css' %}" type="text/css">
|
||||||
<link rel="stylesheet" href="{% static 'css/webhook_form.css' %}" type="text/css">
|
<link rel="stylesheet" href="{% static 'css/webhook_form.css' %}" type="text/css">
|
||||||
|
<link rel="stylesheet" href="{% static 'css/badges.css' %}" type="text/css">
|
||||||
<link rel="stylesheet" href="{% static 'css/base.css' %}" type="text/css">
|
<link rel="stylesheet" href="{% static 'css/base.css' %}" type="text/css">
|
||||||
<link rel="stylesheet" href="{% static 'css/billing.css' %}" type="text/css">
|
<link rel="stylesheet" href="{% static 'css/billing.css' %}" type="text/css">
|
||||||
<link rel="stylesheet" href="{% static 'css/channel_checks.css' %}" type="text/css">
|
<link rel="stylesheet" href="{% static 'css/channel_checks.css' %}" type="text/css">
|
||||||
|
@ -10,15 +10,27 @@
|
|||||||
|
|
||||||
<p id="badges-description">
|
<p id="badges-description">
|
||||||
{{ site_name }} provides status badges for each of the tags
|
{{ site_name }} provides status badges for each of the tags
|
||||||
you have used. Additionally, the "{{ site_name }}"
|
you have used. The badges have public, but hard-to-guess
|
||||||
badge shows the overall status of all checks in a
|
|
||||||
project. The badges have public, but hard-to-guess
|
|
||||||
URLs. You can use them in your READMEs,
|
URLs. You can use them in your READMEs,
|
||||||
dashboards or status pages.
|
dashboards or status pages.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div id="b-format" class="btn-group" data-toggle="buttons">
|
<p>Each badge can be in one of the following states:</p>
|
||||||
<label id="show-svg" class="btn btn-default active">
|
<ul>
|
||||||
|
<li><strong>up</strong> – all matching checks are up.</li>
|
||||||
|
<li><strong>down</strong> – at least one check is currently down.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
As an option, the badges can report a third state:
|
||||||
|
<strong>late</strong> (when at least one check is running late but has not
|
||||||
|
exceeded its grace time yet).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div class="btn-group" data-toggle="buttons">
|
||||||
|
<label id="show-svg" class="btn btn-default active" data->
|
||||||
<input type="radio" autocomplete="off" checked> SVG
|
<input type="radio" autocomplete="off" checked> SVG
|
||||||
</label>
|
</label>
|
||||||
<label id="show-json" class="btn btn-default">
|
<label id="show-json" class="btn btn-default">
|
||||||
@ -28,8 +40,42 @@
|
|||||||
<input type="radio" autocomplete="off"> Shields.io
|
<input type="radio" autocomplete="off"> Shields.io
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<table id="badges-svg" class="badges table">
|
<div class="btn-group" data-toggle="buttons">
|
||||||
|
<label id="show-no-late" class="btn btn-default active">
|
||||||
|
<input type="radio" autocomplete="off" checked> Badge states: <b>up</b> or <b>down</b>
|
||||||
|
</label>
|
||||||
|
<label id="show-with-late" class="btn btn-default">
|
||||||
|
<input type="radio" autocomplete="off"> Badge states: <b>up</b>, <b>late</b> or <b>down</b>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table id="badges-svg" class="table badge-preview">
|
||||||
|
{% if have_tags %}
|
||||||
|
<tr><th colspan="2">Tags</th></tr>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for urldict in badges %}
|
||||||
|
{% if urldict.tag == "*" %}
|
||||||
|
<tr>
|
||||||
|
<th colspan="2">Overall Status</th>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<img class="no-late" src="{{ urldict.svg }}" alt="" />
|
||||||
|
<img class="with-late" src="{{ urldict.svg3 }}" alt="" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<code class="no-late">{{ urldict.svg }}</code>
|
||||||
|
<code class="with-late">{{ urldict.svg3 }}</code>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
<table id="badges-json" class="table badge-preview">
|
||||||
{% if have_tags %}
|
{% if have_tags %}
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="2">Tags</th>
|
<th colspan="2">Tags</th>
|
||||||
@ -45,40 +91,18 @@
|
|||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<img src="{{ urldict.svg }}" alt="" />
|
<code class="fetch-json no-late" data-url="{{ urldict.json }}"></code>
|
||||||
|
<code class="fetch-json with-late" data-url="{{ urldict.json3 }}"></code>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<code>{{ urldict.svg }}</code>
|
<code class="no-late">{{ urldict.json }}</code>
|
||||||
</td>
|
<code class="with-late">{{ urldict.json3 }}</code>
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
<table id="badges-json" class="badges table">
|
|
||||||
{% if have_tags %}
|
|
||||||
<tr>
|
|
||||||
<th colspan="2">Tags</th>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% for urldict in badges %}
|
|
||||||
{% if urldict.tag == "*" %}
|
|
||||||
<tr>
|
|
||||||
<th colspan="2">Overall Status</th>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td class="json-response" data-url="{{ urldict.json }}">
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<code>{{ urldict.json }}</code>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div id="badges-shields">
|
<table id="badges-shields" class="table badge-preview">
|
||||||
<table class="badges table">
|
|
||||||
{% if have_tags %}
|
{% if have_tags %}
|
||||||
<tr>
|
<tr>
|
||||||
<th>Shields.io badge</th>
|
<th>Shields.io badge</th>
|
||||||
@ -95,10 +119,12 @@
|
|||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<img src="https://img.shields.io/endpoint?url={{ urldict.shields|urlencode:"" }}" alt="" />
|
<img class="no-late" src="https://img.shields.io/endpoint?url={{ urldict.shields|urlencode:"" }}" alt="" />
|
||||||
|
<img class="with-late" src="https://img.shields.io/endpoint?url={{ urldict.shields3|urlencode:"" }}" alt="" />
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<code>{{ urldict.shields }}</code>
|
<code class="no-late">{{ urldict.shields }}</code>
|
||||||
|
<code class="with-late">{{ urldict.shields3 }}</code>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user