Healthcheck web improvements + PUID & PGID support added to Docker (#198)

This commit is contained in:
Chris Caron 2024-06-29 22:07:12 -04:00 committed by GitHub
parent 6e57e33b8f
commit c6b9c1161d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 390 additions and 108 deletions

View File

@ -1,4 +1,4 @@
FROM python:3.11-slim as base FROM python:3.11-slim AS base
# set version label # set version label
ARG BUILD_DATE ARG BUILD_DATE
@ -7,13 +7,13 @@ LABEL build_version="Apprise API version:- ${VERSION} Build-date:- ${BUILD_DATE}
LABEL maintainer="Chris-Caron" LABEL maintainer="Chris-Caron"
# set environment variables # set environment variables
ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED 1 ENV PYTHONUNBUFFERED=1
ENV APPRISE_CONFIG_DIR /config ENV APPRISE_CONFIG_DIR=/config
ENV APPRISE_ATTACH_DIR /attach ENV APPRISE_ATTACH_DIR=/attach
ENV APPRISE_PLUGIN_PATHS /plugin ENV APPRISE_PLUGIN_PATHS=/plugin
FROM base as builder FROM base AS builder
WORKDIR /build/ WORKDIR /build/
@ -41,11 +41,11 @@ RUN set -eux && \
--no-binary cryptography \ --no-binary cryptography \
cryptography cryptography
FROM base as runtime FROM base AS runtime
# Install requirements and gunicorn # Install requirements and gunicorn
COPY ./requirements.txt /etc/requirements.txt COPY ./requirements.txt /etc/requirements.txt
COPY --from=builder /build/*.whl . COPY --from=builder /build/*.whl ./
RUN set -eux && \ RUN set -eux && \
echo "Installing cryptography" && \ echo "Installing cryptography" && \
pip3 install *.whl && \ pip3 install *.whl && \
@ -55,6 +55,9 @@ RUN set -eux && \
apt-get update -qq && \ apt-get update -qq && \
apt-get install -y -qq \ apt-get install -y -qq \
nginx && \ nginx && \
echo "Installing tools" && \
apt-get install -y -qq \
sed && \
echo "Cleaning up" && \ echo "Cleaning up" && \
apt-get --yes autoremove --purge && \ apt-get --yes autoremove --purge && \
apt-get clean --yes && \ apt-get clean --yes && \
@ -73,16 +76,13 @@ WORKDIR /opt/apprise
# Copy over Apprise API # Copy over Apprise API
COPY apprise_api/ webapp 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 && \ RUN umask 0002 && \
mkdir -p /attach /config /plugin /run/apprise && \ touch /etc/nginx/server-override.conf && \
chown www-data:www-data -R /run/apprise /var/lib/nginx /attach /config /plugin touch /etc/nginx/location-override.conf
# Handle running as a non-root user (www-data is id/gid 33)
USER www-data
VOLUME /config VOLUME /config
VOLUME /attach VOLUME /attach
VOLUME /plugin VOLUME /plugin
EXPOSE 8000 EXPOSE 8000
CMD ["/usr/local/bin/supervisord", "-c", "/opt/apprise/webapp/etc/supervisord.conf"] CMD ["/opt/apprise/webapp/supervisord-startup"]

104
README.md
View File

@ -56,11 +56,17 @@ docker pull caronc/apprise:latest
# setting APPRISE_STATEFUL_MODE to simple allows you to map your defined {key} # 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 # straight to a file found in the `/config` path. In simple home configurations
# this is sometimes the ideal expectation. # 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 \ docker run --name apprise \
-p 8000:8000 \ -p 8000:8000 \
-v /var/lib/apprise/config:/config \ -e PUID=$(id -u) \
-v /var/lib/apprise/plugin:/plugin \ -e PGID=$(id -g) \
-v /var/lib/apprise/attach:/attach \ -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_STATEFUL_MODE=simple \
-e APPRISE_WORKER_COUNT=1 \ -e APPRISE_WORKER_COUNT=1 \
-d caronc/apprise:latest -d caronc/apprise:latest
@ -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 # Setup your environment the way you like
docker build -t apprise/local:latest -f Dockerfile . 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 # Launch your instance
docker run --name apprise \ docker run --name apprise \
-p 8000:8000 \ -p 8000:8000 \
-e PUID=$(id -u) \
-e PGID=$(id -g) \
-e APPRISE_STATEFUL_MODE=simple \ -e APPRISE_STATEFUL_MODE=simple \
-e APPRISE_WORKER_COUNT=1 \ -e APPRISE_WORKER_COUNT=1 \
-v /etc/apprise:/config \
-d apprise/local:latest -d apprise/local:latest
``` ```
A `docker-compose.yml` file is already set up to grant you an instant production ready simulated environment: A `docker-compose.yml` file is already set up to grant you an instant production ready simulated environment:
@ -86,40 +98,6 @@ A `docker-compose.yml` file is already set up to grant you an instant production
docker-compose up 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 ## Dockerfile Details
The following architectures are supported: `amd64`, `arm/v7`, and `arm64`. The following tags can be used: The following architectures are supported: `amd64`, `arm/v7`, and `arm64`. The following tags can be used:
@ -398,6 +376,8 @@ The use of environment variables allow you to provide over-rides to default sett
| Variable | Description | | 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_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_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`. | `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`.
@ -421,8 +401,56 @@ The use of environment variables allow you to provide over-rides to default sett
| `DEBUG` | This defaults to `no` and can however be set to `yes` by simply defining the global variable as such. | `DEBUG` | This defaults to `no` and can however be set to `yes` by simply defining the global variable as such.
## Development Environment ## Nginx Overrides
The 2 files you can override are:
1. `/etc/nginx/location-override.conf` which is included within all of the Apprise API NginX `location` references.
1. `/etc/nginx/server-override.conf` which is included within Apprise API `server` reference.
### Authentication
Under the hood, Apprise-API is running a small NginX instance. It allows for you to inject your own configuration into it. One thing you may wish to add is basic authentication.
Below we create ourselves some nginx directives we'd like to apply to our Apprise API:
```nginx
# Our override.conf file:
auth_basic "Apprise API Restricted Area";
auth_basic_user_file /etc/nginx/.htpasswd;
```
Now let's set ourselves up with a simple password file (for more info on htpasswd files, see [here](https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/)
```bash
# Create ourselves a for our user 'foobar'; the below will prompt you for the pass
# you want to provide:
htpasswd -c apprise_api.htpasswd foobar
# Note: the -c above is only needed to create the database for the first time
```
Now we can create our docker container with this new authentication information:
```bash
# Create our container containing Basic Auth:
docker run --name apprise \
-p 8000:8000 \
-e PUID=$(id -u) \
-e PGID=$(id -g) \
-v /path/to/local/config:/config \
-v /path/to/local/attach:/attach \
-v ./override.conf:/etc/nginx/location-override.conf:ro \
-v ./apprise_api.htpasswd:/etc/nginx/.htpasswd:ro \
-e APPRISE_STATEFUL_MODE=simple \
-e APPRISE_WORKER_COUNT=1 \
-d caronc/apprise:latest
```
Visit http://localhost:8000 to see if things are working as expected. If you followed the example above, you should log in as the user `foobar` using the credentials you provided the account.
You can add further accounts to the existing database by omitting the `-c` switch:
```bash
# Add another account
htpasswd apprise_api.htpasswd user2
```
## Development Environment
The following should get you a working development environment to test with: The following should get you a working development environment to test with:
```bash ```bash

View File

@ -37,9 +37,9 @@
</a> </a>
<h1>{% trans "Apprise API" %}</h1> <h1>{% trans "Apprise API" %}</h1>
<ul> <ul>
<li>APPRISE v{{APPRISE_VERSION}}</li> <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> <li class="theme"><a href="{{ request.path }}?theme={{request.next_theme}}"><i class="material-icons">invert_colors</i></a></li>
</ul> </ul>
</div> </div>
</div> </div>
<!-- Page Layout here --> <!-- Page Layout here -->
@ -50,10 +50,10 @@
<ul class="collection z-depth-1"> <ul class="collection z-depth-1">
<a class="collection-item" href="{% url 'config' DEFAULT_CONFIG_ID %}"><i class="material-icons">settings</i> <a class="collection-item" href="{% url 'config' DEFAULT_CONFIG_ID %}"><i class="material-icons">settings</i>
{% trans "Configuration Manager" %}</a> {% 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> <a class="collection-item" href="{% url 'config' UNIQUE_CONFIG_ID %}"><i class="material-icons">refresh</i>
{% trans "New Configuration" %}</a> {% trans "New Configuration" %}</a>
{% endif %} {% endif %}
</ul> </ul>
{% endif %} {% endif %}
<ul class="collection z-depth-1"> <ul class="collection z-depth-1">
@ -80,6 +80,28 @@
</div> </div>
<div class="col s9"> <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 %} {% block body %}{% endblock %}
</div> </div>
@ -91,9 +113,37 @@
M.AutoInit(); M.AutoInit();
// highlightjs // highlightjs
hljs.initHighlightingOnLoad(); hljs.initHighlightingOnLoad();
{% block onload %} {% endblock %} {% block onload %}{% endblock %}
// healthcheck
health_check()
}); });
{% block jsfooter %} {% endblock %} {% 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> </script>
</body> </body>

View File

@ -188,7 +188,7 @@
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block jsfooter %} {% block jsfooter %}
{{ block.super }}
{% if STATEFUL_MODE != 'disabled' %} {% if STATEFUL_MODE != 'disabled' %}
function update_count() { function update_count() {
const p_count = document.querySelectorAll('#url-list li.card-panel.selected').length; const p_count = document.querySelectorAll('#url-list li.card-panel.selected').length;
@ -695,8 +695,8 @@ function notify_init() {
{% endblock %} {% endblock %}
{% block onload %} {% block onload %}
{% if STATEFUL_MODE != 'disabled' %}
{{ block.super }} {{ block.super }}
{% if STATEFUL_MODE != 'disabled' %}
document.querySelector('label [for="id_tag"]') document.querySelector('label [for="id_tag"]')
{ {
// create a new div with the class 'chips' assigned to it // create a new div with the class 'chips' assigned to it

View File

@ -1,6 +1,5 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n %} {% load i18n %}
{% block body %} {% block body %}
<h4>{% trans "The Apprise API" %}</h4> <h4>{% trans "The Apprise API" %}</h4>
<p> <p>

View File

@ -64,6 +64,7 @@ class HealthCheckTests(SimpleTestCase):
content = loads(response.content) content = loads(response.content)
assert content == { assert content == {
'config_lock': False, 'config_lock': False,
'attach_lock': False,
'status': { 'status': {
'can_write_config': True, 'can_write_config': True,
'can_write_attach': True, 'can_write_attach': True,
@ -87,6 +88,7 @@ class HealthCheckTests(SimpleTestCase):
content = loads(response.content) content = loads(response.content)
assert content == { assert content == {
'config_lock': True, 'config_lock': True,
'attach_lock': False,
'status': { 'status': {
'can_write_config': False, 'can_write_config': False,
'can_write_attach': True, 'can_write_attach': True,
@ -109,6 +111,7 @@ class HealthCheckTests(SimpleTestCase):
content = loads(response.content) content = loads(response.content)
assert content == { assert content == {
'config_lock': False, 'config_lock': False,
'attach_lock': False,
'status': { 'status': {
'can_write_config': False, 'can_write_config': False,
'can_write_attach': True, 'can_write_attach': True,
@ -131,6 +134,7 @@ class HealthCheckTests(SimpleTestCase):
content = loads(response.content) content = loads(response.content)
assert content == { assert content == {
'config_lock': False, 'config_lock': False,
'attach_lock': True,
'status': { 'status': {
'can_write_config': True, 'can_write_config': True,
'can_write_attach': False, 'can_write_attach': False,
@ -153,9 +157,10 @@ class HealthCheckTests(SimpleTestCase):
content = loads(response.content) content = loads(response.content)
assert content == { assert content == {
'config_lock': False, 'config_lock': False,
'attach_lock': False,
'status': { 'status': {
'can_write_config': True, 'can_write_config': True,
'can_write_attach': False, 'can_write_attach': True,
'details': ['OK'] 'details': ['OK']
} }
} }

View File

@ -181,6 +181,7 @@ class NotifyTests(SimpleTestCase):
# Reset our mock object # Reset our mock object
mock_notify.reset_mock() mock_notify.reset_mock()
# A setting of zero means unlimited attachments are allowed
with override_settings(APPRISE_MAX_ATTACHMENTS=0): with override_settings(APPRISE_MAX_ATTACHMENTS=0):
# Preare our form data # Preare our form data
@ -196,6 +197,67 @@ class NotifyTests(SimpleTestCase):
form = NotifyForm(form_data, attach_data) form = NotifyForm(form_data, attach_data)
assert form.is_valid() 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 # Send our notification
response = self.client.post( response = self.client.post(
'/notify/{}'.format(key), form.cleaned_data) '/notify/{}'.format(key), form.cleaned_data)

View File

@ -207,6 +207,22 @@ class HTTPAttachment(A_MGR['http']):
pass pass
def touch(fname, mode=0o666, dir_fd=None, **kwargs):
"""
Acts like a Linux touch and updates a file with a current timestamp
"""
flags = os.O_CREAT | os.O_APPEND
try:
with os.fdopen(os.open(fname, flags=flags, mode=mode, dir_fd=dir_fd)) as f:
os.utime(f.fileno() if os.utime in os.supports_fd else fname,
dir_fd=None if os.supports_fd else dir_fd, **kwargs)
except OSError:
return False
return True
def parse_attachments(attachment_payload, files_request): def parse_attachments(attachment_payload, files_request):
""" """
Takes the payload provided in a `/notify` call and extracts the Takes the payload provided in a `/notify` call and extracts the
@ -230,15 +246,12 @@ def parse_attachments(attachment_payload, files_request):
count += 1 count += 1
if settings.APPRISE_ATTACH_SIZE <= 0: if settings.APPRISE_ATTACH_SIZE <= 0:
raise ValueError("The attachment size is restricted to 0MB") raise ValueError("Attachment support has been disabled")
if settings.APPRISE_MAX_ATTACHMENTS <= 0 or \ if settings.APPRISE_MAX_ATTACHMENTS > 0 and count > settings.APPRISE_MAX_ATTACHMENTS:
(settings.APPRISE_MAX_ATTACHMENTS > 0 and
count > settings.APPRISE_MAX_ATTACHMENTS):
raise ValueError( raise ValueError(
"There is a maximum of %d attachments" % "There is a maximum of %d attachments" %
settings.APPRISE_MAX_ATTACHMENTS settings.APPRISE_MAX_ATTACHMENTS)
if settings.APPRISE_MAX_ATTACHMENTS > 0 else 0)
if isinstance(attachment_payload, (tuple, list, set)): if isinstance(attachment_payload, (tuple, list, set)):
for no, entry in enumerate(attachment_payload, start=1): for no, entry in enumerate(attachment_payload, start=1):
@ -772,67 +785,70 @@ def healthcheck(lazy=True):
if not (settings.APPRISE_STATEFUL_MODE == AppriseStoreMode.DISABLED or settings.APPRISE_CONFIG_LOCK): if not (settings.APPRISE_STATEFUL_MODE == AppriseStoreMode.DISABLED or settings.APPRISE_CONFIG_LOCK):
# Update our Configuration Check Block # Update our Configuration Check Block
path = os.path.join(ConfigCache.root, '.tmp_healthcheck') path = os.path.join(ConfigCache.root, '.tmp_hc')
if lazy: if lazy:
try: try:
modify_date = datetime.fromtimestamp(os.path.getmtime(path)) modify_date = datetime.fromtimestamp(os.path.getmtime(path))
delta = (datetime.now() - modify_date).total_seconds() delta = (datetime.now() - modify_date).total_seconds()
if delta <= 7200.00: # 2hrs if delta <= 30.00: # 30s
response['can_write_config'] = True response['can_write_config'] = True
except FileNotFoundError: except FileNotFoundError:
# No worries... continue with below testing # No worries... continue with below testing
pass pass
if not response['can_write_config']:
try:
os.makedirs(path, exist_ok=True)
# Write a small file
with tempfile.TemporaryFile(mode='w+b', dir=path) as fp:
# Test writing 1 block
fp.write(b'.')
# Read it back
fp.seek(0)
fp.read(1) == b'.'
# Toggle our status
response['can_write_config'] = True
except OSError: except OSError:
# Permission Issue or something else likely
# We can take an early exit # We can take an early exit
response['details'].append('CONFIG_PERMISSION_ISSUE') response['details'].append('CONFIG_PERMISSION_ISSUE')
if settings.APPRISE_MAX_ATTACHMENTS > 0 and settings.APPRISE_ATTACH_SIZE > 0: if not (response['can_write_config'] or 'CONFIG_PERMISSION_ISSUE' in response['details']):
try:
os.makedirs(ConfigCache.root, exist_ok=True)
if touch(path):
# Toggle our status
response['can_write_config'] = True
else:
# We can take an early exit as there is already a permission issue detected
response['details'].append('CONFIG_PERMISSION_ISSUE')
except OSError:
# We can take an early exit as there is already a permission issue detected
response['details'].append('CONFIG_PERMISSION_ISSUE')
if settings.APPRISE_ATTACH_SIZE > 0:
# Test our ability to access write attachments # Test our ability to access write attachments
# Update our Configuration Check Block # Update our Configuration Check Block
path = os.path.join(settings.APPRISE_ATTACH_DIR, '.tmp_healthcheck') path = os.path.join(settings.APPRISE_ATTACH_DIR, '.tmp_hc')
if lazy: if lazy:
try: try:
modify_date = datetime.fromtimestamp(os.path.getmtime(path)) modify_date = datetime.fromtimestamp(os.path.getmtime(path))
delta = (datetime.now() - modify_date).total_seconds() delta = (datetime.now() - modify_date).total_seconds()
if delta <= 7200.00: # 2hrs if delta <= 30.00: # 30s
response['can_write_attach'] = True response['can_write_attach'] = True
except FileNotFoundError: except FileNotFoundError:
# No worries... continue with below testing # No worries... continue with below testing
pass pass
if not response['can_write_attach']: except OSError:
# We can take an early exit as there is already a permission issue detected
response['details'].append('ATTACH_PERMISSION_ISSUE')
if not (response['can_write_attach'] or 'ATTACH_PERMISSION_ISSUE' in response['details']):
# No lazy mode set or content require a refresh # No lazy mode set or content require a refresh
try: try:
os.makedirs(path, exist_ok=True) os.makedirs(settings.APPRISE_ATTACH_DIR, exist_ok=True)
if touch(path):
# Write a small file
with tempfile.TemporaryFile(mode='w+b', dir=path) as fp:
# Test writing 1 block
fp.write(b'.')
# Read it back
fp.seek(0)
fp.read(1) == b'.'
# Toggle our status # Toggle our status
response['can_write_attach'] = True response['can_write_attach'] = True
else:
# We can take an early exit as there is already a permission issue detected
response['details'].append('ATTACH_PERMISSION_ISSUE')
except OSError: except OSError:
# We can take an early exit # We can take an early exit
response['details'].append('ATTACH_PERMISSION_ISSUE') response['details'].append('ATTACH_PERMISSION_ISSUE')

View File

@ -161,8 +161,8 @@ class HealthCheckView(View):
and ACCEPT_ALL.match(request.headers.get('accept', '')) else \ and ACCEPT_ALL.match(request.headers.get('accept', '')) else \
MIME_IS_JSON.match(request.headers.get('accept', '')) is not None MIME_IS_JSON.match(request.headers.get('accept', '')) is not None
# Run our healthcheck # Run our healthcheck; allow ?force which will cause the check to run each time
response = healthcheck() response = healthcheck(lazy='force' not in request.GET)
# Prepare our response # Prepare our response
status = ResponseCode.okay if 'OK' in response['details'] else ResponseCode.expectation_failed status = ResponseCode.okay if 'OK' in response['details'] else ResponseCode.expectation_failed
@ -172,6 +172,7 @@ class HealthCheckView(View):
return HttpResponse(response, status=status, content_type='text/plain') \ return HttpResponse(response, status=status, content_type='text/plain') \
if not json_response else JsonResponse({ if not json_response else JsonResponse({
'config_lock': settings.APPRISE_CONFIG_LOCK, 'config_lock': settings.APPRISE_CONFIG_LOCK,
'attach_lock': settings.APPRISE_ATTACH_SIZE <= 0,
'status': response, 'status': response,
}, encoder=JSONEncoder, safe=False, status=status) }, encoder=JSONEncoder, safe=False, status=status)

View File

@ -29,4 +29,8 @@ def base_url(request):
""" """
Returns our defined BASE_URL object Returns our defined BASE_URL object
""" """
return {'BASE_URL': settings.BASE_URL} return {
'BASE_URL': settings.BASE_URL,
'CONFIG_DIR': settings.APPRISE_CONFIG_DIR,
'ATTACH_DIR': settings.APPRISE_ATTACH_DIR,
}

View File

@ -16,11 +16,13 @@ http {
types_hash_max_size 2048; types_hash_max_size 2048;
include /etc/nginx/mime.types; include /etc/nginx/mime.types;
default_type application/octet-stream; default_type application/octet-stream;
# Do not display Nginx Version
server_tokens off;
## ##
# Upload Restriction # Upload Restriction
## ##
client_max_body_size 500M; client_max_body_size 500M;
## ##
# Logging Settings # Logging Settings
@ -43,6 +45,10 @@ http {
listen 8000; listen 8000;
listen [::]:8000; listen [::]:8000;
# Allow users to map to this file and provide their own custom
# overrides such as
include /etc/nginx/server-override.conf;
# Main Website # Main Website
location / { location / {
include /etc/nginx/uwsgi_params; include /etc/nginx/uwsgi_params;
@ -52,12 +58,14 @@ http {
proxy_pass http://localhost:8080; proxy_pass http://localhost:8080;
# Give ample time for notifications to fire # Give ample time for notifications to fire
proxy_read_timeout 120s; proxy_read_timeout 120s;
include /etc/nginx/location-override.conf;
} }
# Static Content # Static Content
location /s/ { location /s/ {
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html; index index.html;
include /etc/nginx/location-override.conf;
} }
# 404 error handling # 404 error handling

View File

@ -4,9 +4,11 @@ pidfile=/run/apprise/supervisord.pid
logfile=/dev/null logfile=/dev/null
logfile_maxbytes=0 logfile_maxbytes=0
user=www-data user=www-data
group=www-data
[program:nginx] [program:nginx]
command=/usr/sbin/nginx -c /opt/apprise/webapp/etc/nginx.conf command=/usr/sbin/nginx -c /opt/apprise/webapp/etc/nginx.conf -p /opt/apprise
directory=/opt/apprise
stdout_logfile=/dev/stdout stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0 stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr stderr_logfile=/dev/stderr

View File

@ -238,3 +238,33 @@ code.config-id {
.chip.selected { .chip.selected {
font-weight: 600; font-weight: 600;
} }
#health-check {
background-color: #f883791f;
border-radius: 25px;
padding: 2em;
margin-bottom: 2em;
}
#health-check h4 {
font-size: 30px;
}
#health-check h4 .material-icons {
margin-top: -0.2em;
}
#health-check li .material-icons {
font-size: 30px;
margin-top: -0.2em;
}
#health-check ul {
list-style-type: disc;
padding-left: 2em;
}
#health-check ul strong {
font-weight: 600;
font-size: 1.2rem;
display: block;
}

View File

@ -273,7 +273,6 @@ h5 {
em { em {
color: #5e81ac !important; color: #5e81ac !important;
text-shadow: 6px 3px #2e3440
} }
.card-panel { .card-panel {

78
apprise_api/supervisord-startup Executable file
View File

@ -0,0 +1,78 @@
#!/bin/bash
# Copyright (C) 2024 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
if [ $(id -u) -ne 0 ]; then
echo "You must be root to run this script."
echo "Caution: This should only be ran in a dockerized instance!"
exit 1
fi
# Default values
PUID=${PUID:=1000}
PGID=${PGID:=1000}
# lookup our identifier
GROUP=$(getent group $PGID 2>/dev/null | cut -d: -f1)
[ -z "$GROUP" ] && groupadd --force -g $PGID apprise &>/dev/null && \
GROUP=apprise
USER=$(id -un $PUID 2>/dev/null)
[ $? -ne 0 ] && useradd -M -N \
-o -u $PUID -G $GROUP -c "Apprise API User" -d /opt/apprise apprise && \
USER=apprise
if [ -z "$USER" ]; then
echo "The specified User ID (PUID) of $PUID is invalid; Aborting operation."
exit 1
elif [ -z "$GROUP" ]; then
echo "The specified Group ID (PGID) of $PGID is invalid; Aborting operation."
exit 1
fi
# Ensure our group has been correctly assigned
usermod -a -G $GROUP $USER &>/dev/null
chmod o+w /dev/stdout /dev/stderr
[ ! -d /attach ] && mkdir -p /attach
chown -R $USER:$GROUP /attach
[ ! -d /config ] && mkdir -p /config
chown $USER:$GROUP /config
[ ! -d /plugin ] && mkdir -p /plugin
[ ! -d /run/apprise ] && mkdir -p /run/apprise
# Some Directories require enforced permissions to play it safe
chown $USER:$GROUP -R /run/apprise /var/lib/nginx /opt/apprise
sed -i -e "s/^\(user[ \t]*=[ \t]*\).*$/\1$USER/g" \
/opt/apprise/webapp/etc/supervisord.conf
sed -i -e "s/^\(group[ \t]*=[ \t]*\).*$/\1$GROUP/g" \
/opt/apprise/webapp/etc/supervisord.conf
# Working directory
cd /opt/apprise
# Launch our SupervisorD
/usr/local/bin/supervisord -c /opt/apprise/webapp/etc/supervisord.conf
# Always return our SupervisorD return code
exit $?