Browse Source

Add the SLACK_ENABLED setting

pull/474/head
Pēteris Caune 4 years ago
parent
commit
52435a9a0c
No known key found for this signature in database GPG Key ID: E28D7679E9A9EDE2
15 changed files with 156 additions and 70 deletions
  1. +1
    -0
      CHANGELOG.md
  2. +1
    -0
      docker/.env
  3. +0
    -58
      hc/api/tests/test_notify.py
  4. +91
    -0
      hc/api/tests/test_notify_slack.py
  5. +3
    -0
      hc/api/transports.py
  6. +7
    -0
      hc/front/tests/test_add_slack.py
  7. +6
    -0
      hc/front/tests/test_add_slack_btn.py
  8. +6
    -0
      hc/front/tests/test_add_slack_complete.py
  9. +6
    -0
      hc/front/views.py
  10. +1
    -0
      hc/settings.py
  11. +12
    -5
      templates/docs/self_hosted_configuration.html
  12. +15
    -5
      templates/docs/self_hosted_configuration.md
  13. +3
    -0
      templates/front/channels.html
  14. +2
    -0
      templates/front/welcome.html
  15. +2
    -2
      templates/integrations/add_slack.html

+ 1
- 0
CHANGELOG.md View File

@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file.
- Add experimental Dockerfile and docker-compose.yml - Add experimental Dockerfile and docker-compose.yml
- Add rate limiting for Pushover notifications (6 notifications / user / minute) - Add rate limiting for Pushover notifications (6 notifications / user / minute)
- Add the WEBHOOKS_ENABLED setting (#471) - Add the WEBHOOKS_ENABLED setting (#471)
- Add the SLACK_ENABLED setting (#471)
## Bug Fixes ## Bug Fixes
- Fix unwanted HTML escaping in SMS and WhatsApp notifications - Fix unwanted HTML escaping in SMS and WhatsApp notifications


+ 1
- 0
docker/.env View File

@ -45,6 +45,7 @@ SITE_NAME=Mychecks
SITE_ROOT=http://localhost:8000 SITE_ROOT=http://localhost:8000
SLACK_CLIENT_ID= SLACK_CLIENT_ID=
SLACK_CLIENT_SECRET= SLACK_CLIENT_SECRET=
SLACK_ENABLED=True
TELEGRAM_BOT_NAME=ExampleBot TELEGRAM_BOT_NAME=ExampleBot
TELEGRAM_TOKEN= TELEGRAM_TOKEN=
TRELLO_APP_KEY= TRELLO_APP_KEY=


+ 0
- 58
hc/api/tests/test_notify.py View File

@ -8,7 +8,6 @@ from django.core import mail
from django.utils.timezone import now from django.utils.timezone import now
from hc.api.models import Channel, Check, Notification, TokenBucket from hc.api.models import Channel, Check, Notification, TokenBucket
from hc.test import BaseTestCase from hc.test import BaseTestCase
from requests.exceptions import Timeout
from django.test.utils import override_settings from django.test.utils import override_settings
@ -72,63 +71,6 @@ class NotifyTestCase(BaseTestCase):
self.assertFalse(mock_post.called) self.assertFalse(mock_post.called)
self.assertEqual(Notification.objects.count(), 0) self.assertEqual(Notification.objects.count(), 0)
@patch("hc.api.transports.requests.request")
def test_slack(self, mock_post):
self._setup_data("slack", "123")
mock_post.return_value.status_code = 200
self.channel.notify(self.check)
assert Notification.objects.count() == 1
args, kwargs = mock_post.call_args
payload = kwargs["json"]
attachment = payload["attachments"][0]
fields = {f["title"]: f["value"] for f in attachment["fields"]}
self.assertEqual(fields["Last Ping"], "an hour ago")
@patch("hc.api.transports.requests.request")
def test_slack_with_complex_value(self, mock_post):
v = json.dumps({"incoming_webhook": {"url": "123"}})
self._setup_data("slack", v)
mock_post.return_value.status_code = 200
self.channel.notify(self.check)
assert Notification.objects.count() == 1
args, kwargs = mock_post.call_args
self.assertEqual(args[1], "123")
@patch("hc.api.transports.requests.request")
def test_slack_handles_500(self, mock_post):
self._setup_data("slack", "123")
mock_post.return_value.status_code = 500
self.channel.notify(self.check)
n = Notification.objects.get()
self.assertEqual(n.error, "Received status code 500")
@patch("hc.api.transports.requests.request", side_effect=Timeout)
def test_slack_handles_timeout(self, mock_post):
self._setup_data("slack", "123")
self.channel.notify(self.check)
n = Notification.objects.get()
self.assertEqual(n.error, "Connection timed out")
@patch("hc.api.transports.requests.request")
def test_slack_with_tabs_in_schedule(self, mock_post):
self._setup_data("slack", "123")
self.check.kind = "cron"
self.check.schedule = "*\t* * * *"
self.check.save()
mock_post.return_value.status_code = 200
self.channel.notify(self.check)
self.assertEqual(Notification.objects.count(), 1)
self.assertTrue(mock_post.called)
@patch("hc.api.transports.requests.request") @patch("hc.api.transports.requests.request")
def test_hipchat(self, mock_post): def test_hipchat(self, mock_post):
self._setup_data("hipchat", "123") self._setup_data("hipchat", "123")


+ 91
- 0
hc/api/tests/test_notify_slack.py View File

@ -0,0 +1,91 @@
# coding: utf-8
from datetime import timedelta as td
import json
from unittest.mock import patch
from django.utils.timezone import now
from hc.api.models import Channel, Check, Notification
from hc.test import BaseTestCase
from requests.exceptions import Timeout
from django.test.utils import override_settings
class NotifyTestCase(BaseTestCase):
def _setup_data(self, value, status="down", email_verified=True):
self.check = Check(project=self.project)
self.check.status = status
self.check.last_ping = now() - td(minutes=61)
self.check.save()
self.channel = Channel(project=self.project)
self.channel.kind = "slack"
self.channel.value = value
self.channel.email_verified = email_verified
self.channel.save()
self.channel.checks.add(self.check)
@patch("hc.api.transports.requests.request")
def test_slack(self, mock_post):
self._setup_data("123")
mock_post.return_value.status_code = 200
self.channel.notify(self.check)
assert Notification.objects.count() == 1
args, kwargs = mock_post.call_args
payload = kwargs["json"]
attachment = payload["attachments"][0]
fields = {f["title"]: f["value"] for f in attachment["fields"]}
self.assertEqual(fields["Last Ping"], "an hour ago")
@patch("hc.api.transports.requests.request")
def test_slack_with_complex_value(self, mock_post):
v = json.dumps({"incoming_webhook": {"url": "123"}})
self._setup_data(v)
mock_post.return_value.status_code = 200
self.channel.notify(self.check)
assert Notification.objects.count() == 1
args, kwargs = mock_post.call_args
self.assertEqual(args[1], "123")
@patch("hc.api.transports.requests.request")
def test_slack_handles_500(self, mock_post):
self._setup_data("123")
mock_post.return_value.status_code = 500
self.channel.notify(self.check)
n = Notification.objects.get()
self.assertEqual(n.error, "Received status code 500")
@patch("hc.api.transports.requests.request", side_effect=Timeout)
def test_slack_handles_timeout(self, mock_post):
self._setup_data("123")
self.channel.notify(self.check)
n = Notification.objects.get()
self.assertEqual(n.error, "Connection timed out")
@patch("hc.api.transports.requests.request")
def test_slack_with_tabs_in_schedule(self, mock_post):
self._setup_data("123")
self.check.kind = "cron"
self.check.schedule = "*\t* * * *"
self.check.save()
mock_post.return_value.status_code = 200
self.channel.notify(self.check)
self.assertEqual(Notification.objects.count(), 1)
self.assertTrue(mock_post.called)
@override_settings(SLACK_ENABLED=False)
def test_it_requires_slack_enabled(self):
self._setup_data("123")
self.channel.notify(self.check)
n = Notification.objects.get()
self.assertEqual(n.error, "Slack notifications are not enabled.")

+ 3
- 0
hc/api/transports.py View File

@ -263,6 +263,9 @@ class Webhook(HttpTransport):
class Slack(HttpTransport): class Slack(HttpTransport):
def notify(self, check): def notify(self, check):
if not settings.SLACK_ENABLED:
return "Slack notifications are not enabled."
text = tmpl("slack_message.json", check=check) text = tmpl("slack_message.json", check=check)
payload = json.loads(text) payload = json.loads(text)
return self.post(self.channel.slack_webhook_url, json=payload) return self.post(self.channel.slack_webhook_url, json=payload)


+ 7
- 0
hc/front/tests/test_add_slack.py View File

@ -1,3 +1,4 @@
from django.test.utils import override_settings
from hc.api.models import Channel from hc.api.models import Channel
from hc.test import BaseTestCase from hc.test import BaseTestCase
@ -38,3 +39,9 @@ class AddSlackTestCase(BaseTestCase):
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
r = self.client.get(self.url) r = self.client.get(self.url)
self.assertEqual(r.status_code, 403) self.assertEqual(r.status_code, 403)
@override_settings(SLACK_ENABLED=False)
def test_it_handles_disabled_integration(self):
self.client.login(username="[email protected]", password="password")
r = self.client.get(self.url)
self.assertEqual(r.status_code, 404)

+ 6
- 0
hc/front/tests/test_add_slack_btn.py View File

@ -34,3 +34,9 @@ class AddSlackBtnTestCase(BaseTestCase):
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
r = self.client.get(self.url) r = self.client.get(self.url)
self.assertEqual(r.status_code, 403) self.assertEqual(r.status_code, 403)
@override_settings(SLACK_ENABLED=False)
def test_it_handles_disabled_integration(self):
self.client.login(username="[email protected]", password="password")
r = self.client.get(self.url)
self.assertEqual(r.status_code, 404)

+ 6
- 0
hc/front/tests/test_add_slack_complete.py View File

@ -81,3 +81,9 @@ class AddSlackCompleteTestCase(BaseTestCase):
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
r = self.client.get("/integrations/add_slack_btn/?code=12345678&state=foo") r = self.client.get("/integrations/add_slack_btn/?code=12345678&state=foo")
self.assertEqual(r.status_code, 403) self.assertEqual(r.status_code, 403)
@override_settings(SLACK_ENABLED=False)
def test_it_requires_slack_enabled(self):
self.client.login(username="[email protected]", password="password")
r = self.client.get("/integrations/add_slack_btn/?code=12345678&state=foo")
self.assertEqual(r.status_code, 404)

+ 6
- 0
hc/front/views.py View File

@ -300,6 +300,7 @@ def index(request):
"enable_pushover": settings.PUSHOVER_API_TOKEN is not None, "enable_pushover": settings.PUSHOVER_API_TOKEN is not None,
"enable_shell": settings.SHELL_ENABLED is True, "enable_shell": settings.SHELL_ENABLED is True,
"enable_signal": settings.SIGNAL_CLI_ENABLED is True, "enable_signal": settings.SIGNAL_CLI_ENABLED is True,
"enable_slack": settings.SLACK_ENABLED is True,
"enable_slack_btn": settings.SLACK_CLIENT_ID is not None, "enable_slack_btn": settings.SLACK_CLIENT_ID is not None,
"enable_sms": settings.TWILIO_AUTH is not None, "enable_sms": settings.TWILIO_AUTH is not None,
"enable_telegram": settings.TELEGRAM_TOKEN is not None, "enable_telegram": settings.TELEGRAM_TOKEN is not None,
@ -767,6 +768,7 @@ def channels(request, code):
"enable_pushover": settings.PUSHOVER_API_TOKEN is not None, "enable_pushover": settings.PUSHOVER_API_TOKEN is not None,
"enable_shell": settings.SHELL_ENABLED is True, "enable_shell": settings.SHELL_ENABLED is True,
"enable_signal": settings.SIGNAL_CLI_ENABLED is True, "enable_signal": settings.SIGNAL_CLI_ENABLED is True,
"enable_slack": settings.SLACK_ENABLED is True,
"enable_slack_btn": settings.SLACK_CLIENT_ID is not None, "enable_slack_btn": settings.SLACK_CLIENT_ID is not None,
"enable_sms": settings.TWILIO_AUTH is not None, "enable_sms": settings.TWILIO_AUTH is not None,
"enable_telegram": settings.TELEGRAM_TOKEN is not None, "enable_telegram": settings.TELEGRAM_TOKEN is not None,
@ -1112,6 +1114,7 @@ def add_pagertree(request, code):
return render(request, "integrations/add_pagertree.html", ctx) return render(request, "integrations/add_pagertree.html", ctx)
@require_setting("SLACK_ENABLED")
@login_required @login_required
def add_slack(request, code): def add_slack(request, code):
project = _get_rw_project_for_user(request, code) project = _get_rw_project_for_user(request, code)
@ -1136,12 +1139,14 @@ def add_slack(request, code):
return render(request, "integrations/add_slack.html", ctx) return render(request, "integrations/add_slack.html", ctx)
@require_setting("SLACK_ENABLED")
@require_setting("SLACK_CLIENT_ID") @require_setting("SLACK_CLIENT_ID")
def slack_help(request): def slack_help(request):
ctx = {"page": "channels"} ctx = {"page": "channels"}
return render(request, "integrations/add_slack_btn.html", ctx) return render(request, "integrations/add_slack_btn.html", ctx)
@require_setting("SLACK_ENABLED")
@require_setting("SLACK_CLIENT_ID") @require_setting("SLACK_CLIENT_ID")
@login_required @login_required
def add_slack_btn(request, code): def add_slack_btn(request, code):
@ -1166,6 +1171,7 @@ def add_slack_btn(request, code):
return render(request, "integrations/add_slack_btn.html", ctx) return render(request, "integrations/add_slack_btn.html", ctx)
@require_setting("SLACK_ENABLED")
@require_setting("SLACK_CLIENT_ID") @require_setting("SLACK_CLIENT_ID")
@login_required @login_required
def add_slack_complete(request): def add_slack_complete(request):


+ 1
- 0
hc/settings.py View File

@ -221,6 +221,7 @@ SIGNAL_CLI_ENABLED = envbool("SIGNAL_CLI_ENABLED", "False")
# Slack integration # Slack integration
SLACK_CLIENT_ID = os.getenv("SLACK_CLIENT_ID") SLACK_CLIENT_ID = os.getenv("SLACK_CLIENT_ID")
SLACK_CLIENT_SECRET = os.getenv("SLACK_CLIENT_SECRET") SLACK_CLIENT_SECRET = os.getenv("SLACK_CLIENT_SECRET")
SLACK_ENABLED = envbool("SLACK_ENABLED", "True")
# Telegram integration -- override in local_settings.py # Telegram integration -- override in local_settings.py
TELEGRAM_BOT_NAME = os.getenv("TELEGRAM_BOT_NAME", "ExampleBot") TELEGRAM_BOT_NAME = os.getenv("TELEGRAM_BOT_NAME", "ExampleBot")


+ 12
- 5
templates/docs/self_hosted_configuration.html View File

@ -267,10 +267,14 @@ its web UI and documentation.</p>
it needs to construct absolute URLs.</p> it needs to construct absolute URLs.</p>
<h2 id="SLACK_CLIENT_ID"><code>SLACK_CLIENT_ID</code></h2> <h2 id="SLACK_CLIENT_ID"><code>SLACK_CLIENT_ID</code></h2>
<p>Default: <code>None</code></p> <p>Default: <code>None</code></p>
<p>The Slack Client ID, required by the Slack integration.</p>
<p>Go to <a href="https://api.slack.com/apps/">https://api.slack.com/apps/</a>
to create a <em>Slack app</em>, and look up its <em>Client ID</em> and <em>Client Secret</em>.</p>
<p>When setting up the Slack app, make sure to:</p>
<p>The Slack Client ID, used by the Slack integration.</p>
<p>The Slack integration can work with or without the Slack Client ID. If
the Slack Client ID is not set, in the "Integrations - Add Slack" page,
Healthchecks will ask the user to provide a webhook URL for posting notifications.</p>
<p>If the Slack Client <em>is</em> set, Healthchecks will use the OAuth2 flow
to get the webhook URL from Slack. The OAuth2 flow is more user-friendly.
To set it up, go to <a href="https://api.slack.com/apps/">https://api.slack.com/apps/</a>
and create a <em>Slack app</em>. When setting up the Slack app, make sure to:</p>
<ul> <ul>
<li>Add the <a href="https://api.slack.com/scopes/incoming-webhook">incoming-webhook</a> <li>Add the <a href="https://api.slack.com/scopes/incoming-webhook">incoming-webhook</a>
scope to the Bot Token Scopes.</li> scope to the Bot Token Scopes.</li>
@ -280,8 +284,11 @@ to create a <em>Slack app</em>, and look up its <em>Client ID</em> and <em>Clien
</ul> </ul>
<h2 id="SLACK_CLIENT_SECRET"><code>SLACK_CLIENT_SECRET</code></h2> <h2 id="SLACK_CLIENT_SECRET"><code>SLACK_CLIENT_SECRET</code></h2>
<p>Default: <code>None</code></p> <p>Default: <code>None</code></p>
<p>The Slack Client Secret, required by the Slack integration.
<p>The Slack Client Secret, required if <code>SLACK_CLIENT_ID</code> is set.
Look it up at <a href="https://api.slack.com/apps/">https://api.slack.com/apps/</a>.</p> Look it up at <a href="https://api.slack.com/apps/">https://api.slack.com/apps/</a>.</p>
<h2 id="SLACK_ENABLED"><code>SLACK_ENABLED</code></h2>
<p>Default: <code>True</code></p>
<p>A boolean that turns on/off the Slack integration. Enabled by default.</p>
<h2 id="TELEGRAM_BOT_NAME"><code>TELEGRAM_BOT_NAME</code></h2> <h2 id="TELEGRAM_BOT_NAME"><code>TELEGRAM_BOT_NAME</code></h2>
<p>Default: <code>ExampleBot</code></p> <p>Default: <code>ExampleBot</code></p>
<p>The <a href="https://telegram.org/">Telegram</a> bot name, required by the Telegram integration.</p> <p>The <a href="https://telegram.org/">Telegram</a> bot name, required by the Telegram integration.</p>


+ 15
- 5
templates/docs/self_hosted_configuration.md View File

@ -427,12 +427,16 @@ it needs to construct absolute URLs.
Default: `None` Default: `None`
The Slack Client ID, required by the Slack integration.
The Slack Client ID, used by the Slack integration.
Go to [https://api.slack.com/apps/](https://api.slack.com/apps/)
to create a _Slack app_, and look up its _Client ID_ and _Client Secret_.
The Slack integration can work with or without the Slack Client ID. If
the Slack Client ID is not set, in the "Integrations - Add Slack" page,
Healthchecks will ask the user to provide a webhook URL for posting notifications.
When setting up the Slack app, make sure to:
If the Slack Client _is_ set, Healthchecks will use the OAuth2 flow
to get the webhook URL from Slack. The OAuth2 flow is more user-friendly.
To set it up, go to [https://api.slack.com/apps/](https://api.slack.com/apps/)
and create a _Slack app_. When setting up the Slack app, make sure to:
* Add the [incoming-webhook](https://api.slack.com/scopes/incoming-webhook) * Add the [incoming-webhook](https://api.slack.com/scopes/incoming-webhook)
scope to the Bot Token Scopes. scope to the Bot Token Scopes.
@ -444,9 +448,15 @@ When setting up the Slack app, make sure to:
Default: `None` Default: `None`
The Slack Client Secret, required by the Slack integration.
The Slack Client Secret. Required if `SLACK_CLIENT_ID` is set.
Look it up at [https://api.slack.com/apps/](https://api.slack.com/apps/). Look it up at [https://api.slack.com/apps/](https://api.slack.com/apps/).
## `SLACK_ENABLED` {: #SLACK_ENABLED }
Default: `True`
A boolean that turns on/off the Slack integration. Enabled by default.
## `TELEGRAM_BOT_NAME` {: #TELEGRAM_BOT_NAME } ## `TELEGRAM_BOT_NAME` {: #TELEGRAM_BOT_NAME }
Default: `ExampleBot` Default: `ExampleBot`


+ 3
- 0
templates/front/channels.html View File

@ -186,6 +186,7 @@
{% if rw %} {% if rw %}
<h1 class="ai-title">Add More</h1> <h1 class="ai-title">Add More</h1>
<ul class="add-integration"> <ul class="add-integration">
{% if enable_slack %}
<li> <li>
<img src="{% static 'img/integrations/slack.png' %}" <img src="{% static 'img/integrations/slack.png' %}"
class="icon" alt="Slack icon" /> class="icon" alt="Slack icon" />
@ -198,6 +199,8 @@
<a href="{% url 'hc-add-slack' project.code %}" class="btn btn-primary">Add Integration</a> <a href="{% url 'hc-add-slack' project.code %}" class="btn btn-primary">Add Integration</a>
{% endif %} {% endif %}
</li> </li>
{% endif %}
<li> <li>
<img src="{% static 'img/integrations/email.png' %}" <img src="{% static 'img/integrations/email.png' %}"
class="icon" alt="Email icon" /> class="icon" alt="Email icon" />


+ 2
- 0
templates/front/welcome.html View File

@ -393,6 +393,7 @@
</div> </div>
{% endif %} {% endif %}
{% if enable_slack %}
<div class="col-lg-2 col-md-3 col-sm-4 col-xs-6"> <div class="col-lg-2 col-md-3 col-sm-4 col-xs-6">
{% if enable_slack_btn %} {% if enable_slack_btn %}
<a href="{% url 'hc-slack-help' %}" class="integration"> <a href="{% url 'hc-slack-help' %}" class="integration">
@ -412,6 +413,7 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% endif %}
{% if enable_apprise %} {% if enable_apprise %}
<div class="col-lg-2 col-md-3 col-sm-4 col-xs-6"> <div class="col-lg-2 col-md-3 col-sm-4 col-xs-6">


+ 2
- 2
templates/integrations/add_slack.html View File

@ -40,7 +40,7 @@
<div class="row ai-step"> <div class="row ai-step">
<div class="col-sm-6"> <div class="col-sm-6">
<span class="step-no"></span> <span class="step-no"></span>
<p>Copy the displayed <strong>WebHook URL</strong> and paste it down below.</p>
<p>Copy the displayed <strong>Webhook URL</strong> and paste it down below.</p>
<p>Save the integration, and it's done!</p> <p>Save the integration, and it's done!</p>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
@ -57,7 +57,7 @@
{% csrf_token %} {% csrf_token %}
<div class="form-group {{ form.value.css_classes }}"> <div class="form-group {{ form.value.css_classes }}">
<label for="callback-url" class="col-sm-2 control-label"> <label for="callback-url" class="col-sm-2 control-label">
Callback URL
Webhook URL
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<input <input


Loading…
Cancel
Save