diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 9ba499f34..020d6191a 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -7,5 +7,6 @@ "streetsidesoftware.code-spell-checker", "qwtel.sqlite-viewer", "jebbs.markdown-extended", + "william-voyek.vscode-nginx", ] } \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index b45a17c8f..40ec49b21 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,6 +16,25 @@ "autoStartBrowser": false, "program": "${workspaceFolder}/app/manage.py" }, + { + "name": "Debug: Gunicorn", + "type": "debugpy", + "request": "launch", + "module": "gunicorn", + "args": [ + "--access-logfile", + "-", + "--workers", + "3", + "--bind", + "0.0.0.0:8002", + "app.wsgi:application", + + ], + "django": true, + "autoStartBrowser": false, + "cwd": "${workspaceFolder}/app" + }, { "name": "Debug: Celery", "type": "python", diff --git a/Release-Notes.md b/Release-Notes.md index dc2bda58b..419b958d9 100644 --- a/Release-Notes.md +++ b/Release-Notes.md @@ -1,3 +1,19 @@ +# Version 1.3.0 + +!!! danger "Security" + As is currently the recommended method of deployment, the Centurion Container must be deployed behind a reverse proxy the conducts the SSL termination. + +This release updates the docker container to be a production setup for deployment of Centurion. Prior to this version Centurion ERP was using a development setup for the webserver. + +- Docker now uses SupervisorD for container + +- Gunicorn WSGI setup for Centurion with NginX as the webserver. + +- Container now has a health check. + +- To setup container as "Worker", set `IS_WORKER='True'` environmental variable within container. _**Note:** You can still use command `celery -A app worker -l INFO`, although **not** recommended as the container health check will not be functioning_ + + # Version 1.0.0 Initial Release of Centurion ERP. diff --git a/app/access/models.py b/app/access/models.py index f0acca2a9..a46332da4 100644 --- a/app/access/models.py +++ b/app/access/models.py @@ -106,7 +106,9 @@ def get_queryset(self): if request: - user = request.user._wrapped if hasattr(request.user,'_wrapped') else request.user + # user = request.user._wrapped if hasattr(request.user,'_wrapped') else request.user + + user = request.user if user.is_authenticated: diff --git a/dockerfile b/dockerfile index 45d9d6b18..0664b582c 100644 --- a/dockerfile +++ b/dockerfile @@ -2,7 +2,11 @@ ARG CI_PROJECT_URL='' ARG CI_COMMIT_SHA='' ARG CI_COMMIT_TAG='' -FROM python:3.11-alpine3.19 as build +ARG ALPINE_VERSION=3.20 +ARG NGINX_VERSION=1.27.2-r1 +ARG PYTHON_VERSION=3.11.10 + +FROM python:${PYTHON_VERSION}-alpine${ALPINE_VERSION} as build RUN pip --disable-pip-version-check list --outdated --format=json | \ @@ -27,8 +31,24 @@ RUN apk add --update \ pkgconf \ postgresql16-dev \ postgresql16-client \ - libpq-dev + libpq-dev \ + # NginX: to download items + openssl \ + curl \ + ca-certificates + +RUN printf "%s%s%s%s\n" \ + "@nginx " \ + "http://nginx.org/packages/mainline/alpine/v" \ + `egrep -o '^[0-9]+\.[0-9]+' /etc/alpine-release` \ + "/main" \ + | tee -a /etc/apk/repositories + +RUN curl -o /tmp/nginx_signing.rsa.pub https://nginx.org/keys/nginx_signing.rsa.pub; \ + openssl rsa -pubin -in /tmp/nginx_signing.rsa.pub -text -noout; + + RUN pip install --upgrade \ setuptools \ wheel \ @@ -60,7 +80,7 @@ RUN cd /tmp/python_modules \ -FROM python:3.11-alpine3.19 +FROM python:${PYTHON_VERSION}-alpine${ALPINE_VERSION} LABEL \ org.opencontainers.image.vendor="No Fuss Computing" \ @@ -74,10 +94,15 @@ ARG CI_PROJECT_URL ARG CI_COMMIT_SHA ARG CI_COMMIT_TAG +ARG NGINX_VERSION + ENV CI_PROJECT_URL=${CI_PROJECT_URL} ENV CI_COMMIT_SHA=${CI_COMMIT_SHA} ENV CI_COMMIT_TAG=${CI_COMMIT_TAG} +ENV IS_WORKER=False + + COPY requirements.txt requirements.txt COPY requirements_test.txt requirements_test.txt @@ -86,6 +111,11 @@ COPY ./app/. app COPY --from=build /tmp/python_builds /tmp/python_builds +COPY --from=build /etc/apk/repositories /etc/apk/repositories + +COPY --from=build /tmp/nginx_signing.rsa.pub /etc/apk/keys/nginx_signing.rsa.pub + + COPY includes/ / RUN pip --disable-pip-version-check list --outdated --format=json | \ @@ -95,19 +125,36 @@ RUN pip --disable-pip-version-check list --outdated --format=json | \ apk add --no-cache \ mariadb-client \ mariadb-dev \ - postgresql16-client; \ + postgresql16-client \ + nginx@nginx=${NGINX_VERSION}; \ pip install --no-cache-dir /tmp/python_builds/*.*; \ python /app/manage.py collectstatic --noinput; \ rm -rf /tmp/python_builds; \ + rm /etc/nginx/sites-enabled; \ + rm /etc/nginx/conf.d/default.conf; \ + mv /etc/nginx/conf.d/centurion.conf /etc/nginx/conf.d/default.conf; \ + # Check for errors and fail if so + nginx -t; \ + # sanity check, https://github.com/nofusscomputing/centurion_erp/pull/370 + if [ ! $(python -m django --version) ]; then \ + echo "Django not Installed"; \ + exit 1; \ + fi; \ + chmod +x /entrypoint.sh; \ + mkdir -p /etc/supervisor/conf.d; \ export WORKDIR /app - +# In future, adjust port to 80 as nginX is now used (Will be breaking change) EXPOSE 8000 VOLUME [ "/data", "/etc/itsm" ] -CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD \ + supervisorctl status || exit 1 + + + ENTRYPOINT ["/entrypoint.sh"] diff --git a/docs/projects/centurion_erp/administration/installation.md b/docs/projects/centurion_erp/administration/installation.md index b1ddffd36..7378e1c6e 100644 --- a/docs/projects/centurion_erp/administration/installation.md +++ b/docs/projects/centurion_erp/administration/installation.md @@ -8,6 +8,8 @@ about: https://gitlab.com/nofusscomputing/infrastructure/configuration-managemen Centurion ERP is a simple application to deploy with the only additional requirements being that you have already deployed a database server and a RabbitMQ server. Centurion ERP is container based and is deployable via Docker or upon Kubernetes. Our images are available on [Docker Hub](https://hub.docker.com/r/nofusscomputing/centurion-erp). +Deployment of Centurion ERP is recommended to be behind a reverse proxy. This is required as the method used to setup the containers does not include any SSL setup. Due to this the reverse proxy will be required to conduct the SSL termination. + !!! note "TL;DR" `docker pull nofusscomputing/centurion-erp:latest`. @@ -50,7 +52,7 @@ The [web container](https://hub.docker.com/r/nofusscomputing/centurion-erp) is t ### Background Worker Container -The [Background Worker container](https://hub.docker.com/r/nofusscomputing/centurion-erp) is a worker that waits for tasks sent to the RabbitMQ server. The worker is based upon [Celery](https://docs.celeryq.dev/en/stable/index.html). On the worker not being busy, it'll pickup and run the task. This container is scalable with nil additional requirements for launching additional workers. If deploying to Kubernetes the setting the deployment `replicas` to the number of desired containers is the simplest method to scale. The container start command will need to be set to `celery -A app worker -l INFO` so that the worker is started on container startup. +The [Background Worker container](https://hub.docker.com/r/nofusscomputing/centurion-erp) is a worker that waits for tasks sent to the RabbitMQ server. The worker is based upon [Celery](https://docs.celeryq.dev/en/stable/index.html). On the worker not being busy, it'll pickup and run the task. This container is scalable with nil additional requirements for launching additional workers. If deploying to Kubernetes the setting the deployment `replicas` to the number of desired containers is the simplest method to scale. There is no container start command, however you will need to set environmental variable `IS_WORKER` to value `'True'` within the container. Configuration for the worker resides in directory `/etc/itsm/` within the container. see below for the `CELERY_` configuration. diff --git a/includes/entrypoint.sh b/includes/entrypoint.sh new file mode 100644 index 000000000..be1ffb85b --- /dev/null +++ b/includes/entrypoint.sh @@ -0,0 +1,75 @@ +#!/bin/sh + +set -e + +mkdir -p /etc/supervisor/conf.d; + +if [ "$1" == "" ]; then + + + echo "[Info] Setup SupervisorD" + + if [ ${IS_WORKER} == 'True' ] || [ ${IS_WORKER} == 'true' ]; then + + + echo '[info] Creating worker service config'; + + cp /etc/supervisor/conf.source/worker.conf /etc/supervisor/conf.d/worker.conf; + + if [ -f '/etc/supervisor/conf.d/worker.conf' ]; then + + echo '[info] Worker service config Created'; + + else + + echo '[crit] Worker service config not created'; + + fi; + + + else + + echo '[info] Creating gunicorn service config'; + + cp /etc/supervisor/conf.source/gunicorn.conf /etc/supervisor/conf.d/gunicorn.conf; + + if [ -f '/etc/supervisor/conf.d/gunicorn.conf' ]; then + + echo '[info] Gunicorn service config Created'; + + else + + echo '[crit] Gunicorn service config not created'; + + fi; + + + echo '[info] Creating nginx service config'; + + cp /etc/supervisor/conf.source/nginx.conf /etc/supervisor/conf.d/nginx.conf; + + if [ -f '/etc/supervisor/conf.d/nginx.conf' ]; then + + echo '[info] NginX service config Created'; + + else + + echo '[crit] NginX service config not created'; + + fi; + + + fi; + + + echo "[Info] SupervisorD Setup successfully" + + + /usr/local/bin/supervisord; + + +else + + exec "$@" + +fi diff --git a/includes/etc/nginx/conf.d/centurion.conf b/includes/etc/nginx/conf.d/centurion.conf new file mode 100644 index 000000000..b69b689a2 --- /dev/null +++ b/includes/etc/nginx/conf.d/centurion.conf @@ -0,0 +1,21 @@ +server { + + listen 8000; + + location = /favicon.ico { access_log off; log_not_found off; } + + location /static/ { + + alias /app/static/; + + } + + location / { + + include proxy_params; + + proxy_pass http://unix:/run/gunicorn.sock; + + } + +} \ No newline at end of file diff --git a/includes/etc/nginx/proxy_params b/includes/etc/nginx/proxy_params new file mode 100644 index 000000000..df75bc5d7 --- /dev/null +++ b/includes/etc/nginx/proxy_params @@ -0,0 +1,4 @@ +proxy_set_header Host $http_host; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme; diff --git a/includes/etc/supervisor/conf.d/.gitkeep b/includes/etc/supervisor/conf.d/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/includes/etc/supervisor/conf.source/gunicorn.conf b/includes/etc/supervisor/conf.source/gunicorn.conf new file mode 100644 index 000000000..1e9b866ee --- /dev/null +++ b/includes/etc/supervisor/conf.source/gunicorn.conf @@ -0,0 +1,10 @@ +[program:gunicorn] +priority=1 +startsecs=0 +stopwaitsecs=55 +autostart=true +autorestart=true +stdout_logfile=/var/log/%(program_name)s.log +stderr_logfile=/var/log/%(program_name)s.log +directory=/app +command=gunicorn --access-logfile - --workers 10 --bind unix:/run/gunicorn.sock app.wsgi:application diff --git a/includes/etc/supervisor/conf.source/nginx.conf b/includes/etc/supervisor/conf.source/nginx.conf new file mode 100644 index 000000000..0e50f1206 --- /dev/null +++ b/includes/etc/supervisor/conf.source/nginx.conf @@ -0,0 +1,8 @@ +[program:nginx] +startsecs=0 +stopwaitsecs=55 +command=nginx -g "daemon off;" +autorestart=true +autostart=true +stdout_logfile=/var/log/%(program_name)s.log +stderr_logfile=/var/log/%(program_name)s.log diff --git a/includes/etc/supervisor/conf.source/worker.conf b/includes/etc/supervisor/conf.source/worker.conf new file mode 100644 index 000000000..936d15e52 --- /dev/null +++ b/includes/etc/supervisor/conf.source/worker.conf @@ -0,0 +1,10 @@ +[program:celery] +priority=1 +startsecs=0 +stopwaitsecs=55 +autostart=true +autorestart=true +stdout_logfile=/var/log/%(program_name)s.log +stderr_logfile=/var/log/%(program_name)s.log +directory=/app +command=celery -A app worker -l INFO diff --git a/includes/etc/supervisor/supervisord.conf b/includes/etc/supervisor/supervisord.conf new file mode 100644 index 000000000..b01663101 --- /dev/null +++ b/includes/etc/supervisor/supervisord.conf @@ -0,0 +1,33 @@ + +[unix_http_server] +file=/run/supervisor.sock ; (the path to the socket file) +chmod=0700 ; sockef file mode (default 0700) + +;[inet_http_server] +;port = :9001 +; username = user +; password = 123 + +[supervisord] +logfile=/var/log/supervisord.log +pidfile=/run/supervisord.pid +childlogdir=/var/log +nodaemon = true + +; the below section must remain in the config file for RPC +; (supervisorctl/web interface) to work, additional interfaces may be +; added by defining them in separate rpcinterface: sections +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +[supervisorctl] +serverurl=unix:///run/supervisor.sock ; use a unix:// URL for a unix socket + +; The [include] section can just contain the "files" setting. This +; setting can list multiple files (separated by whitespace or +; newlines). It can also contain wildcards. The filenames are +; interpreted as relative to this file. Included files *cannot* +; include files themselves. + +[include] +files = /etc/supervisor/conf.d/*.conf diff --git a/requirements_production.txt b/requirements_production.txt index 30f924c41..4a418fd9a 100644 --- a/requirements_production.txt +++ b/requirements_production.txt @@ -2,4 +2,10 @@ mysqlclient==2.2.4 # Postgres support -psycopg2==2.9.9 # postgresql16-dev postgresql16-client libpq-dev \ No newline at end of file +psycopg2==2.9.9 # postgresql16-dev postgresql16-client libpq-dev + +# Production Web server +gunicorn==23.0.0 + +# SupervisorD +supervisor==4.2.5