From 82424cdd5e808612a447f9de93fe33183a23e161 Mon Sep 17 00:00:00 2001 From: Timothy Hobbs Date: Tue, 19 Sep 2023 16:56:48 +0200 Subject: [PATCH] Add standalone installation option with preconfigured docker-compose --- standalone/.gitignore | 2 + standalone/Caddyfile | 3 + standalone/Dockerfile | 19 ++ standalone/README.md | 40 ++++ standalone/__init__.py | 0 standalone/config/__init__.py | 0 standalone/config/settings.py | 236 +++++++++++++++++++++++ standalone/config/urls.py | 32 +++ standalone/config/wsgi.py | 17 ++ standalone/custom_navigation_header.html | 10 + standalone/docker-compose.yml | 28 +++ standalone/docker.env.template | 1 + standalone/entrypoint.sh | 22 +++ standalone/extra-requirements.txt | 3 + standalone/manage.py | 27 +++ standalone/setup.sh | 8 + 16 files changed, 448 insertions(+) create mode 100644 standalone/.gitignore create mode 100644 standalone/Caddyfile create mode 100644 standalone/Dockerfile create mode 100644 standalone/README.md create mode 100644 standalone/__init__.py create mode 100644 standalone/config/__init__.py create mode 100644 standalone/config/settings.py create mode 100644 standalone/config/urls.py create mode 100644 standalone/config/wsgi.py create mode 100644 standalone/custom_navigation_header.html create mode 100644 standalone/docker-compose.yml create mode 100644 standalone/docker.env.template create mode 100644 standalone/entrypoint.sh create mode 100644 standalone/extra-requirements.txt create mode 100755 standalone/manage.py create mode 100755 standalone/setup.sh diff --git a/standalone/.gitignore b/standalone/.gitignore new file mode 100644 index 00000000..618abc79 --- /dev/null +++ b/standalone/.gitignore @@ -0,0 +1,2 @@ +docker.env +db \ No newline at end of file diff --git a/standalone/Caddyfile b/standalone/Caddyfile new file mode 100644 index 00000000..ca83029f --- /dev/null +++ b/standalone/Caddyfile @@ -0,0 +1,3 @@ +localhost { + reverse_proxy helpdesk:8000 +} diff --git a/standalone/Dockerfile b/standalone/Dockerfile new file mode 100644 index 00000000..968129c5 --- /dev/null +++ b/standalone/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.10-slim-bullseye +LABEL src=https://github.com/django-helpdesk/django-helpdesk +RUN apt-get update +RUN apt-get install -yqq \ + postgresql-common \ + postgresql-client \ + cron +COPY requirements.txt /opt/django-helpdesk/requirements.txt +COPY standalone/extra-requirements.txt /opt/django-helpdesk/standalone/extra-requirements.txt +RUN pip3 install -r /opt/django-helpdesk/requirements.txt +RUN pip3 install -r /opt/django-helpdesk/standalone/extra-requirements.txt +COPY . /opt/django-helpdesk +WORKDIR /opt/django-helpdesk +RUN pip3 install -e . +RUN DJANGO_HELPDESK_SECRET_KEY=foo python3 standalone/manage.py collectstatic + +RUN echo "* * * * * root . /etc/env && /usr/local/bin/python3 /opt/django-helpdesk/standalone/manage.py get_email >> /var/log/cron.log 2>&1" > /etc/crontab +RUN chmod 0644 /etc/crontab +ENTRYPOINT sh /opt/django-helpdesk/standalone/entrypoint.sh \ No newline at end of file diff --git a/standalone/README.md b/standalone/README.md new file mode 100644 index 00000000..6ccf34ec --- /dev/null +++ b/standalone/README.md @@ -0,0 +1,40 @@ +Django-helpdesk standalone +------------------------------- + +This is a standalone installation of Django-helpdesk allowing you to run django-helpdesk as a production standalone application in docker. + +To install run `setup.sh` and then `docker-compose up` in this directory. + + +To create an admin user exec into the newly created container + + docker ps + docker exec -it standalone-web-1 bash + +In the container cd to `/opt/django-helpdesk/standalone` and run + + python3 manage.py createsuperuser + +You should now be able to log in to the server by visiting `localhost:80`. You will also need to access the `/admin` url to set up new users. + +Configuration for production use +-------------------------------------- + +For production use you will need to change the URL from `localhost` in the `Caddyfile`. You will also need to update the `docker-compose` file to fix paths. By default all files are stored in `/tmp`. + +You should be able to set custom settings by bindmounting a `local_settings.py` file into `/opt/django-helpdesk/standalone/config/local_settings.py` + +You can change the logo at the top left of the helpdesk by bindmounting a file into `/opt/django-helpdesk/helpdesk/templates/helpdesk/custom_navigation_header.html` with contents like: + +``` + +``` diff --git a/standalone/__init__.py b/standalone/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/standalone/config/__init__.py b/standalone/config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/standalone/config/settings.py b/standalone/config/settings.py new file mode 100644 index 00000000..715372f9 --- /dev/null +++ b/standalone/config/settings.py @@ -0,0 +1,236 @@ +""" +Django settings for django-helpdesk demodesk project. + +For more information on this file, see +https://docs.djangoproject.com/en/1.11/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.11/ref/settings/ +""" + + +import os + + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ + +# Read SECRET_KEY from DJANGO_HELPDESK_SECRET_KEY env var +try: + SECRET_KEY = os.environ['DJANGO_HELPDESK_SECRET_KEY'] +except KeyError: + raise Exception("DJANGO_HELPDESK_SECRET_KEY environment variable is not set") + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = False + +ALLOWED_HOSTS = os.environ.get("DJANGO_HELPDESK_ALLOWED_HOSTS", "*, localhost, 0.0.0.0").split(",") + +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.sites', + 'django.contrib.humanize', + 'bootstrap4form', + 'account', # Required by pinax-teams + 'pinax.invitations', # required by pinax-teams + 'pinax.teams', # team support + 'reversion', # required by pinax-teams + 'helpdesk', # This is us! + 'rest_framework', # required for the API +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "whitenoise.middleware.WhiteNoiseMiddleware", +] + +ROOT_URLCONF = 'demo.demodesk.config.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'debug': True, + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'standalone.config.wsgi.application' + + +# django-helpdesk configuration settings +# You can override django-helpdesk's defaults by redefining them here. +# To see what settings are available, see the docs/configuration.rst +# file for more information. +# Some common settings are below. + +HELPDESK_DEFAULT_SETTINGS = { + 'use_email_as_submitter': os.environ.get('HELPDESK_USE_EMAIL_AS_SUBMITTER', 'True') == 'True', + 'email_on_ticket_assign': os.environ.get('HELPDESK_EMAIL_ON_TICKET_ASSIGN', 'True') == 'True', + 'email_on_ticket_change': os.environ.get('HELPDESK_EMAIL_ON_TICKET_CHANGE', 'True') == 'True', + 'login_view_ticketlist': os.environ.get('HELPDESK_LOGIN_VIEW_TICKETLIST', 'True') == 'True', + 'email_on_ticket_apichange': os.environ.get('HELPDESK_EMAIL_ON_TICKET_APICHANGE', 'True') == 'True', + 'preset_replies': os.environ.get('HELPDESK_PRESET_REPLIES', 'True') == 'True', + 'tickets_per_page': os.environ.get('HELPDESK_TICKETS_PER_PAGE', '25'), +} + +# Should the public web portal be enabled? +HELPDESK_PUBLIC_ENABLED = os.environ.get('HELPDESK_PUBLIC_ENABLED', 'True') == 'True' +HELPDESK_VIEW_A_TICKET_PUBLIC = os.environ.get('HELPDESK_VIEW_A_TICKET_PUBLIC', 'True') == 'True' +HELPDESK_SUBMIT_A_TICKET_PUBLIC = os.environ.get('HELPDESK_SUBMIT_A_TICKET_PUBLIC', 'True') == 'True' + +# Should the Knowledgebase be enabled? +HELPDESK_KB_ENABLED = os.environ.get('HELPDESK_KB_ENABLED', 'True') == 'True' + +HELPDESK_TICKETS_TIMELINE_ENABLED = os.environ.get('HELPDESK_TICKETS_TIMELINE_ENABLED', 'True') == 'True' + +# Allow users to change their passwords +HELPDESK_SHOW_CHANGE_PASSWORD = os.environ.get('HELPDESK_SHOW_CHANGE_PASSWORD', 'True') == 'True' + +# Activate the API +HELPDESK_ACTIVATE_API_ENDPOINT = os.environ.get('HELPDESK_ACTIVATE_API_ENDPOINT', 'True') == 'True' + +# Instead of showing the public web portal first, +# we can instead redirect users straight to the login page. +HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = os.environ.get('HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT', 'False') == 'True' +LOGIN_URL = 'helpdesk:login' +LOGIN_REDIRECT_URL = 'helpdesk:home' + + +DATABASES = { + # Setup postgress db with postgres as host and db name and read password from env var + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.environ.get('POSTGRES_DB', 'postgres'), + 'USER': os.environ.get('POSTGRES_USER', 'postgres'), + 'PASSWORD': os.environ.get('POSTGRES_PASSWORD', 'postgres'), + 'HOST': os.environ.get('POSTGRES_HOST', 'postgres'), + 'PORT': os.environ.get('POSTGRES_PORT', '5432'), + } +} + + +# Sites +# - this allows hosting of more than one site from a single server, +# in practice you can probably just leave this default if you only +# host a single site, but read more in the docs: +# https://docs.djangoproject.com/en/1.11/ref/contrib/sites/ + +SITE_ID = 1 + + +# Sessions +# https://docs.djangoproject.com/en/1.11/topics/http/sessions + +SESSION_COOKIE_AGE = 86400 # = 1 day + +# Password validation +# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +# Email +# https://docs.djangoproject.com/en/1.11/topics/email/ + +# This demo uses the console backend, which simply prints emails to the console +# rather than actually sending them out. +DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'example@example.com') +SERVER_EMAIL = os.environ.get('SERVER_EMAIL', 'example@example.com') + +if os.environ.get('EMAIL_HOST', None): + EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' + try: + EMAIL_HOST = os.environ['EMAIL_HOST'] + except KeyError: + raise ImproperlyConfigured('Please set the EMAIL_HOST environment variable.') + try: + EMAIL_PORT = os.environ['EMAIL_PORT'] + except KeyError: + raise ImproperlyConfigured('Please set the EMAIL_PORT environment variable.') +else: + EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' + +# Internationalization +# https://docs.djangoproject.com/en/1.11/topics/i18n/ + +# By default, django-helpdesk uses en, but other languages are also available. +# The most complete translations are: es-MX, ru, zh-Hans +# Contribute to our translations via Transifex if you can! +# See CONTRIBUTING.rst for more info. +LANGUAGE_CODE = 'en-US' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.11/howto/static-files/ +def normpath(*args): + return os.path.normpath(os.path.abspath(os.path.join(*args))) + + +PROJECT_ROOT = normpath(__file__, "..", "..") +STATIC_ROOT = os.environ.get("DJANGO_HELPDESK_STATIC_ROOT", normpath(PROJECT_ROOT, "static")) +STATIC_URL = os.environ.get("DJANGO_HELPDESK_STATIC_URL", "/static/") + + +# MEDIA_ROOT is where media uploads are stored. +# We set this to a directory to host file attachments created +# with tickets. +MEDIA_URL = '/media/' +MEDIA_ROOT = '/data/media' + +# for Django 3.2+, set default for autofields: +DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' + +try: + from .local_settings import * +except ImportError: + pass diff --git a/standalone/config/urls.py b/standalone/config/urls.py new file mode 100644 index 00000000..2fb2a3d7 --- /dev/null +++ b/standalone/config/urls.py @@ -0,0 +1,32 @@ +"""django-helpdesk demodesk URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.10/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.conf.urls import url, include + 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +from django.conf import settings +from django.conf.urls.static import static +from django.contrib import admin +from django.urls import include, path + + +# The following uses the static() helper function, +# which only works when in development mode (using DEBUG). +# For a real deployment, you'd have to properly configure a media server. +# For more information, see: +# https://docs.djangoproject.com/en/1.10/howto/static-files/ + +urlpatterns = [ + path('admin/', admin.site.urls), + path('', include('helpdesk.urls', namespace='helpdesk')), + path('api/auth/', include('rest_framework.urls', namespace='rest_framework')) +] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/standalone/config/wsgi.py b/standalone/config/wsgi.py new file mode 100644 index 00000000..ef704eb8 --- /dev/null +++ b/standalone/config/wsgi.py @@ -0,0 +1,17 @@ +""" +WSGI config for django-helpdesk demodesk project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ +""" + + +from django.core.wsgi import get_wsgi_application +import os + + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "standalone.config.settings") + +application = get_wsgi_application() diff --git a/standalone/custom_navigation_header.html b/standalone/custom_navigation_header.html new file mode 100644 index 00000000..a9e905db --- /dev/null +++ b/standalone/custom_navigation_header.html @@ -0,0 +1,10 @@ + diff --git a/standalone/docker-compose.yml b/standalone/docker-compose.yml new file mode 100644 index 00000000..bbeb246e --- /dev/null +++ b/standalone/docker-compose.yml @@ -0,0 +1,28 @@ +version: '2' +services: + caddy: + image: caddy:2 + restart: unless-stopped + volumes: + - /tmp/data/caddy/data:/data + - /tmp/data/caddy/config:/config + - ./Caddyfile:/etc/caddy/Caddyfile:r + ports: + - "80:80" + - "443:443" + + django-helpdesk: + build: + context: .. + dockerfile: standalone/Dockerfile + user: root + volumes: + - /tmp/django-helpdesk-data:/data/ + - ./custom_navigation_header.html:/opt/django-helpdesk/helpdesk/templates/helpdesk/custom_navigation_header.html:r + env_file: docker.env + + postgres: + image: postgres:12-bullseye + volumes: + - ./db:/var/lib/postgresql/data + env_file: docker.env diff --git a/standalone/docker.env.template b/standalone/docker.env.template new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/standalone/docker.env.template @@ -0,0 +1 @@ + diff --git a/standalone/entrypoint.sh b/standalone/entrypoint.sh new file mode 100644 index 00000000..26719cf3 --- /dev/null +++ b/standalone/entrypoint.sh @@ -0,0 +1,22 @@ +#!/bin/bash +cd /opt/django-helpdesk/standalone/ +if python manage.py showmigrations | grep '\[ \]\|^[a-z]' | grep '[ ]' -B 1; then + python manage.py migrate --noinput # Apply database migrations +fi + +# Starting cron to check emails +printenv > /etc/env +env | awk -F= '{printf "export %s=\"%s\"\n", $1, $2}' > /etc/env +cron && +# Start Gunicorn processes +echo Starting Gunicorn. +exec gunicorn standalone.config.wsgi:application \ + --name django-helpdesk \ + --bind 0.0.0.0:${GUNICORN_PORT:-"8000"} \ + --workers ${GUNICORN_NUM_WORKERS:-"6"} \ + --timeout ${GUNICORN_TIMEOUT:-"60"} \ + --preload \ + --log-level=debug \ + --log-file=- \ + --access-logfile=- \ + "$@" diff --git a/standalone/extra-requirements.txt b/standalone/extra-requirements.txt new file mode 100644 index 00000000..a4961749 --- /dev/null +++ b/standalone/extra-requirements.txt @@ -0,0 +1,3 @@ +whitenoise +gunicorn +psycopg2-binary diff --git a/standalone/manage.py b/standalone/manage.py new file mode 100755 index 00000000..36efe400 --- /dev/null +++ b/standalone/manage.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +import os +import sys + + +def main(): + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "standalone.config.settings") + try: + from django.core.management import execute_from_command_line + except ImportError: + # The above import may fail for some other reason. Ensure that the + # issue is really that Django is missing to avoid masking other + # exceptions on Python 2. + try: + import django + except ImportError: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) + raise + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/standalone/setup.sh b/standalone/setup.sh new file mode 100755 index 00000000..87d3891f --- /dev/null +++ b/standalone/setup.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +# if docker.env does not exist create it from the template +if [ ! -f docker.env ]; then + cp docker.env.template docker.env + echo "DJANGO_HELPDESK_SECRET_KEY="$(mcookie) >> docker.env + echo "POSTGRES_PASSWORD="$(mcookie) >> docker.env +fi