Skip to content

Commit

Permalink
Healthcheck web improvements + PUID & PGID support added to Docker
Browse files Browse the repository at this point in the history
  • Loading branch information
caronc committed Jun 30, 2024
1 parent 6e57e33 commit 48514c0
Show file tree
Hide file tree
Showing 15 changed files with 325 additions and 96 deletions.
15 changes: 7 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ FROM base as runtime

# Install requirements and gunicorn
COPY ./requirements.txt /etc/requirements.txt
COPY --from=builder /build/*.whl .
COPY --from=builder /build/*.whl ./
RUN set -eux && \
echo "Installing cryptography" && \
pip3 install *.whl && \
Expand All @@ -55,6 +55,9 @@ RUN set -eux && \
apt-get update -qq && \
apt-get install -y -qq \
nginx && \
echo "Installing tools" && \
apt-get install -y -qq \
sed && \
echo "Cleaning up" && \
apt-get --yes autoremove --purge && \
apt-get clean --yes && \
Expand All @@ -73,16 +76,12 @@ WORKDIR /opt/apprise
# Copy over Apprise API
COPY apprise_api/ webapp

#
# # Configuration Permissions (to run nginx as a non-root user)
# Configuration Permissions (to run nginx as a non-root user)
RUN umask 0002 && \
mkdir -p /attach /config /plugin /run/apprise && \
chown www-data:www-data -R /run/apprise /var/lib/nginx /attach /config /plugin
touch /etc/nginx/override.conf

# Handle running as a non-root user (www-data is id/gid 33)
USER www-data
VOLUME /config
VOLUME /attach
VOLUME /plugin
EXPOSE 8000
CMD ["/usr/local/bin/supervisord", "-c", "/opt/apprise/webapp/etc/supervisord.conf"]
CMD ["/opt/apprise/webapp/supervisord-startup"]
54 changes: 17 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,17 @@ docker pull caronc/apprise:latest
# setting APPRISE_STATEFUL_MODE to simple allows you to map your defined {key}
# straight to a file found in the `/config` path. In simple home configurations
# this is sometimes the ideal expectation.
#
# Set your User ID or Group ID if you wish to over-ride the default of 1000
# in the below example, we make sure it runs as the user we created the container as

docker run --name apprise \
-p 8000:8000 \
-v /var/lib/apprise/config:/config \
-v /var/lib/apprise/plugin:/plugin \
-v /var/lib/apprise/attach:/attach \
-e PUID=$(id -u) \
-e PGID=$(id -g) \
-v /path/to/local/config:/config \
-v /path/to/local/plugin:/plugin \
-v /path/to/local/attach:/attach \
-e APPRISE_STATEFUL_MODE=simple \
-e APPRISE_WORKER_COUNT=1 \
-d caronc/apprise:latest
Expand All @@ -72,11 +78,17 @@ A common change one might make is to update the Dockerfile to point to the maste
# Setup your environment the way you like
docker build -t apprise/local:latest -f Dockerfile .

# Set up a directory you wish to store your configuration in:
mkdir -p /etc/apprise

# Launch your instance
docker run --name apprise \
-p 8000:8000 \
-e PUID=$(id -u) \
-e PGID=$(id -g) \
-e APPRISE_STATEFUL_MODE=simple \
-e APPRISE_WORKER_COUNT=1 \
-v /etc/apprise:/config \
-d apprise/local:latest
```
A `docker-compose.yml` file is already set up to grant you an instant production ready simulated environment:
Expand All @@ -86,40 +98,6 @@ A `docker-compose.yml` file is already set up to grant you an instant production
docker-compose up
```

### Config Directory Permissions

Under the hood, An NginX services is reading/writing your configuration files as the user (and group) `www-data` which generally has the id of `33`. In preparation so that you don't get the error: `An error occured saving configuration.` consider also setting up your local `/var/lib/apprise/config` permissions as:

```bash
# Create a user/group (if one doesn't already exist) owned
# by the user and group id of 33
id 33 &>/dev/null || sudo useradd \
--system --no-create-home --shell /bin/false \
-u 33 -g 33 www-data

# Securely set the directory limiting access to only those who
# are part of the www-data group:
sudo chmod 770 -R /var/lib/apprise/config
sudo chown 33:33 -R /var/lib/apprise/config

# Now optionally add yourself to the group if you wish to be able to view
# contents.
sudo usermod -a -G 33 $(whoami)

# You may need to log out and back in again for the above usermod
# to reflect on you. Alternatively you can just type the following
# and it will work as a temporary solution:
sudo su - $(whoami)
```

Alternatively a dirty solution is to just set the directory with full read/write permissions (which is not ideal in a production environment):

```bash
# Grant full permission to the local directory you're saving your
# Apprise configuration to:
chmod 777 /var/lib/apprise/config
```

## Dockerfile Details

The following architectures are supported: `amd64`, `arm/v7`, and `arm64`. The following tags can be used:
Expand Down Expand Up @@ -398,6 +376,8 @@ The use of environment variables allow you to provide over-rides to default sett

| Variable | Description |
|--------------------- | ----------- |
| `PUID` | The User ID you wish the Apprise instance under the hood to run as. The default is `1000` if not otherwise specified.
| `PGID` | The Group ID you wish the Apprise instance under the hood to run as. The default is `1000` if not otherwise specified.
| `APPRISE_DEFAULT_THEME` | Can be set to `light` or `dark`; it defaults to `light` if not otherwise provided. The theme can be toggled from within the website as well.
| `APPRISE_DEFAULT_CONFIG_ID` | Defaults to `apprise`. This is the presumed configuration ID you always default to when accessing the configuration manager via the website.
| `APPRISE_CONFIG_DIR` | Defines an (optional) persistent store location of all configuration files saved. By default:<br/> - Configuration is written to the `apprise_api/var/config` directory when just using the _Django_ `manage runserver` script. However for the path for the container is `/config`.
Expand Down
62 changes: 56 additions & 6 deletions apprise_api/api/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@
</a>
<h1>{% trans "Apprise API" %}</h1>
<ul>
<li>APPRISE v{{APPRISE_VERSION}}</li>
<li class="theme"><a href="{{ request.path }}?theme={{request.next_theme}}"><i class="material-icons">invert_colors</i></a></li>
</ul>
<li>APPRISE v{{APPRISE_VERSION}}</li>
<li class="theme"><a href="{{ request.path }}?theme={{request.next_theme}}"><i class="material-icons">invert_colors</i></a></li>
</ul>
</div>
</div>
<!-- Page Layout here -->
Expand All @@ -50,10 +50,10 @@ <h1>{% trans "Apprise API" %}</h1>
<ul class="collection z-depth-1">
<a class="collection-item" href="{% url 'config' DEFAULT_CONFIG_ID %}"><i class="material-icons">settings</i>
{% trans "Configuration Manager" %}</a>
{% if not CONFIG_LOCK %}
{% if not CONFIG_LOCK %}
<a class="collection-item" href="{% url 'config' UNIQUE_CONFIG_ID %}"><i class="material-icons">refresh</i>
{% trans "New Configuration" %}</a>
{% endif %}
{% endif %}
</ul>
{% endif %}
<ul class="collection z-depth-1">
Expand All @@ -80,6 +80,28 @@ <h1>{% trans "Apprise API" %}</h1>
</div>

<div class="col s9">
<div id="health-check" class="section" style="display: none">
<h4><i class="material-icons" style="color: orange">warning</i>&nbsp;{% trans "Apprise Health Check Failed" %}&nbsp;<i class="material-icons" style="color: orange">warning</i></h4>
{% blocktrans %}The following disk access errors have been detected with your Apprise instance{% endblocktrans %}:
<ul>
<li class="can_write_config" style="display: none"><strong>
<i class="material-icons"
style="color: red">cancel</i>&nbsp;{% trans "Configuration Write Failure" %}</strong>
<p>{% blocktrans %}Apprise can not write new configuration information to the directory:{% endblocktrans %} <code>{{CONFIG_DIR}}</code>.</p>
<p>{% blocktrans %}<em>Note:</em> If this is the expected behavior, you should pre-set the environment variable <code>APPRISE_CONFIG_LOCK=yes</code> and reload your Apprise instance.{% endblocktrans %}</p>
</li>

<li class="can_write_attach" style="display: none"><strong>
<i class="material-icons"
style="color: red">cancel</i>&nbsp;{% trans "Attachment Temporary Storage Write Failure" %}</strong>
<p>{% blocktrans %}Apprise can not circulate attachments (if provided) along to supported endpoints due to not having write access to the directory:{% endblocktrans %} <code>{{ATTACH_DIR}}</code>.</p>
<p>{% blocktrans %}<em>Note:</em> If this is the expected behavior, you should pre-set the environment variable <code>APPRISE_ATTACH_SIZE=0</code> and reload your Apprise instance.{% endblocktrans %}</p>
</p>
</li>
</ul>
<p>{% blocktrans %}Under most circumstances, the issue(s) identified here are usually related to permission issues. Make sure you set the correct <code>PUID</code> and <code>GUID</code> to reflect the permissions you wish Apprise to utilize when it is reading and writing its files. In addition to this, you may need to make sure the permissions are set correctly on the directories you mapped them too.{% endblocktrans %}</p>
<p>{% blocktrans %}The issue(s) identified here can also be associated with SELinux too. You may wish to rule out SELinux by first temporarily disabling it using the command <code>setenforce 0</code>. You can always re-enstate it with <code>setenforce 1</code>{% endblocktrans %}.</p>
</div>
{% block body %}{% endblock %}
</div>

Expand All @@ -91,9 +113,37 @@ <h1>{% trans "Apprise API" %}</h1>
M.AutoInit();
// highlightjs
hljs.initHighlightingOnLoad();
{% block onload %} {% endblock %}
{% block onload %}{% endblock %}
// healthcheck
health_check()
});
{% block jsfooter %} {% endblock %}
function health_check() {
// perform our health check
document.querySelector('#health-check').style.display = 'none';
document.querySelector('#health-check li.can_write_config').style.display = 'none';
document.querySelector('#health-check li.can_write_attach').style.display = 'none';
let response = fetch('{% url "health" %}', {
method: 'GET',
headers: {
'Accept': 'application/json;charset=utf-8'
},

}).then(function(response) {
if(response.status != 200)
{
response.json().then(function(content) {
if (content['status']['can_write_config'] === false && content['config_lock'] === false) {
document.querySelector('#health-check li.can_write_config').style.display = '';
}
if (content['status']['can_write_attach'] === false && content['attach_lock'] === false) {
document.querySelector('#health-check li.can_write_attach').style.display = '';
}
document.querySelector('#health-check').style.display = '';
})
}
});
}
</script>
</body>

Expand Down
4 changes: 2 additions & 2 deletions apprise_api/api/templates/config.html
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ <h4>{% trans "Persistent Store Endpoints" %}</h4>
{% endif %}
{% endblock %}
{% block jsfooter %}

{{ block.super }}
{% if STATEFUL_MODE != 'disabled' %}
function update_count() {
const p_count = document.querySelectorAll('#url-list li.card-panel.selected').length;
Expand Down Expand Up @@ -695,8 +695,8 @@ <h4>{% trans "Persistent Store Endpoints" %}</h4>
{% endblock %}

{% block onload %}
{% if STATEFUL_MODE != 'disabled' %}
{{ block.super }}
{% if STATEFUL_MODE != 'disabled' %}
document.querySelector('label [for="id_tag"]')
{
// create a new div with the class 'chips' assigned to it
Expand Down
1 change: 0 additions & 1 deletion apprise_api/api/templates/welcome.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{% extends 'base.html' %}
{% load i18n %}

{% block body %}
<h4>{% trans "The Apprise API" %}</h4>
<p>
Expand Down
7 changes: 6 additions & 1 deletion apprise_api/api/tests/test_healthecheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def test_healthcheck_simple(self):
content = loads(response.content)
assert content == {
'config_lock': False,
'attach_lock': False,
'status': {
'can_write_config': True,
'can_write_attach': True,
Expand All @@ -87,6 +88,7 @@ def test_healthcheck_simple(self):
content = loads(response.content)
assert content == {
'config_lock': True,
'attach_lock': False,
'status': {
'can_write_config': False,
'can_write_attach': True,
Expand All @@ -109,6 +111,7 @@ def test_healthcheck_simple(self):
content = loads(response.content)
assert content == {
'config_lock': False,
'attach_lock': False,
'status': {
'can_write_config': False,
'can_write_attach': True,
Expand All @@ -131,6 +134,7 @@ def test_healthcheck_simple(self):
content = loads(response.content)
assert content == {
'config_lock': False,
'attach_lock': True,
'status': {
'can_write_config': True,
'can_write_attach': False,
Expand All @@ -153,9 +157,10 @@ def test_healthcheck_simple(self):
content = loads(response.content)
assert content == {
'config_lock': False,
'attach_lock': False,
'status': {
'can_write_config': True,
'can_write_attach': False,
'can_write_attach': True,
'details': ['OK']
}
}
Expand Down
62 changes: 62 additions & 0 deletions apprise_api/api/tests/test_notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ def test_notify_by_loaded_urls(self, mock_notify):
# Reset our mock object
mock_notify.reset_mock()

# A setting of zero means unlimited attachments are allowed
with override_settings(APPRISE_MAX_ATTACHMENTS=0):

# Preare our form data
Expand All @@ -196,6 +197,67 @@ def test_notify_by_loaded_urls(self, mock_notify):
form = NotifyForm(form_data, attach_data)
assert form.is_valid()

# Send our notification
response = self.client.post(
'/notify/{}'.format(key), form.cleaned_data)

# We're good!
assert response.status_code == 200
assert mock_notify.call_count == 1

# Reset our mock object
mock_notify.reset_mock()

# Only allow 1 attachment, but we'll attempt to send more...
with override_settings(APPRISE_MAX_ATTACHMENTS=1):

# Preare our form data
form_data = {
'body': 'test notifiction',
}

# At a minimum, just a body is required
form = NotifyForm(form_data)

assert form.is_valid()
# Required to prevent None from being passed into self.client.post()
del form.cleaned_data['attachment']

data = {
**form.cleaned_data,
'file1': SimpleUploadedFile(
"attach1.txt", b"content here", content_type="text/plain"),
'file2': SimpleUploadedFile(
"attach2.txt", b"more content here", content_type="text/plain"),
}

# Send our notification
response = self.client.post(
'/notify/{}'.format(key), data, format='multipart')

# Too many attachments
assert response.status_code == 400
assert mock_notify.call_count == 0

# Reset our mock object
mock_notify.reset_mock()

# A setting of zero means unlimited attachments are allowed
with override_settings(APPRISE_ATTACH_SIZE=0):

# Preare our form data
form_data = {
'body': 'test notifiction',
}
attach_data = {
'attachment': SimpleUploadedFile(
"attach.txt", b"content here", content_type="text/plain")
}

# At a minimum, just a body is required
form = NotifyForm(form_data, attach_data)
assert form.is_valid()

# Send our notification
response = self.client.post(
'/notify/{}'.format(key), form.cleaned_data)
Expand Down
Loading

0 comments on commit 48514c0

Please sign in to comment.