diff --git a/.environment-dev b/.environment-dev new file mode 100644 index 0000000000000000000000000000000000000000..335bee02784270b902cb18a538adb44da03977dc --- /dev/null +++ b/.environment-dev @@ -0,0 +1,9 @@ +UWSGI_PROCESS_PER_CONTAINER=4 +DEBUG=true +ALLOWED_HOSTS=localhost,127.0.0.1 +SECRET_KEY=Ixosoh1iemoh0Heloh1thee5akooboonu5veehae4aikoh2ohg +DATABASE_HOST=shellvalier-postgresql +DATABASE_NAME=shellvalier +DATABASE_USER=shellvalier +DATABASE_PASSWORD=shellvalier +DATABASE_PORT=5432 diff --git a/.gitignore b/.gitignore index 41529f75e0b508f1f71bd0e45a78cdc46d651107..84e6ffe8d9ce7bc8261581bdf88cdcbb89a85220 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ *.pyc */__pycache__/* */migrations/*_auto_*.py +/.datastore/ +/.idea/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..53e1671306bb33855ed3945915ee076d29291997 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,77 @@ +FROM ubuntu:focal + +MAINTAINER Dariusz Czerski <dcz@ipipan.waw.pl> + +ENV DEBIAN_FRONTEND=noninteractive \ + LANG=en_US.UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANGUAGE=en_US:en \ + TZ=Europe/Warsaw \ + PYTHONUNBUFFERED=1 \ + PYTHONFAULTHANDLER=1 + +ENV PACKAGES="\ + binutils \ + curl \ + gdal-bin \ + gettext \ + git \ + libproj-dev \ + locales \ + nginx \ + postgresql-client \ + python3-pip \ + python3-setuptools \ + python3-wheel \ + python3.8 \ + python3.8-dev \ + syslinux \ + tar \ + tzdata \ + unzip \ + wget \ + " + +ENV DEV_PACKAGES="\ + gpg-agent \ + libcurl4-openssl-dev \ + libssl-dev \ + software-properties-common \ + " + +ADD requirements.txt /requirements.txt + +RUN echo $TZ > /etc/timezone && \ + ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \ + apt update && \ + apt install -y -f --no-install-recommends $PACKAGES $DEV_PACKAGES && \ + update-alternatives --install /usr/bin/python python /usr/bin/python3.8 1 && \ + update-alternatives --install /usr/bin/pip pip /usr/bin/pip3 1 && \ + sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \ + echo "LC_ALL=$LC_ALL" >> /etc/environment && \ + echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen && \ + echo "LANG=$LANG" > /etc/locale.conf && \ + locale-gen $LANG && \ + pip install -r /requirements.txt && \ + wget -O - http://download.sgjp.pl/apt/sgjp.gpg.key | apt-key add - && \ + apt-add-repository http://download.sgjp.pl/apt/ubuntu && \ + apt update && \ + apt install -y -f --no-install-recommends morfeusz2 && \ + wget http://download.sgjp.pl/morfeusz/20211017/Linux/20.04/64/morfeusz2-1.99.1-20211017-cp35.cp36.cp37.cp38.cp39-abi3-linux_x86_64.whl && \ + pip install morfeusz2-1.99.1-20211017-cp35.cp36.cp37.cp38.cp39-abi3-linux_x86_64.whl && \ + rm morfeusz2-1.99.1-20211017-cp35.cp36.cp37.cp38.cp39-abi3-linux_x86_64.whl && \ + mkdir /run/nginx/ && \ + apt purge $DEV_PACKAGES -y && \ + apt autoremove --purge -y && \ + rm -rf /root/.cache/ && \ + rm -rf /usr/src/ && \ + rm -rf /var/lib/apt/lists/* + + +ADD docker/config/uwsgi.ini /uwsgi.ini +ADD . /app + +VOLUME /app +WORKDIR /app + +ENTRYPOINT ["/app/docker/scripts/docker-entrypoint"] diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ba18189880d170a05d564219716df3c82f7dd1a4 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# ShellValier v2 + +## Running the development environment + +In order to run the development environment locally: +1. Make sure you have [Docker Desktop](https://docs.docker.com/desktop/) and [Docker Compose](https://docs.docker.com/compose/) installed. +2. Build and run the project by executing: + + ./docker/scripts/run-docker + +## Working in the development environment + +Whenever you need to establish an interactive bash session in the running applicaiton container, execute: + + ./docker/scripts/docker-bash diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..338cabc96379180aedfa3408330369236147bf70 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +version: "3" + +networks: + app-tier: + driver: bridge + +services: + postgresql: + container_name: shellvalier-postgresql + image: postgres:14.2 + environment: + POSTGRES_USER: "shellvalier" + POSTGRES_DB: "shellvalier" + POSTGRES_PASSWORD: "shellvalier" + POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=en_US.UTF-8 --lc-ctype=en_US.UTF-8" + networks: + - app-tier + volumes: + - ./.datastore/postgresql:/var/lib/postgresql/data + + backend: + container_name: shellvalier-backend + image: shellvalier-developer:latest + depends_on: + - postgresql + networks: + - app-tier + env_file: .environment-dev + volumes: + - .:/app + ports: + - "8000:8000" diff --git a/docker/config/uwsgi.ini b/docker/config/uwsgi.ini new file mode 100644 index 0000000000000000000000000000000000000000..4011356c26918e280eccdb98c50c8f070a279929 --- /dev/null +++ b/docker/config/uwsgi.ini @@ -0,0 +1,21 @@ +[uwsgi] +chdir = /app +wsgi-file = /app/shellvalier/wsgi.py +socket = /run/uwsgi.sock +pidfile = /run/uwsgi.pid + +chmod-socket = 666 + +master = true +processes = UWSGI_PROCESS_PER_CONTAINER +enable-threads = true +lazy-apps = true + +max-requests = 500 +max-requests-delta = 10 + +buffer-size = 32768 + +harakiri = 300 + +disable-write-exception = true diff --git a/docker/scripts/docker-bash b/docker/scripts/docker-bash new file mode 100755 index 0000000000000000000000000000000000000000..7c6db1887115c53fdf1d994b838391a3a232a164 --- /dev/null +++ b/docker/scripts/docker-bash @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +docker-compose exec backend bash diff --git a/docker/scripts/docker-entrypoint b/docker/scripts/docker-entrypoint new file mode 100755 index 0000000000000000000000000000000000000000..873a4186ae4a37593e9f701547d1400ca78dc724 --- /dev/null +++ b/docker/scripts/docker-entrypoint @@ -0,0 +1,23 @@ +#!/bin/bash +echo 'Starting docker container' +set -e + +function assertPresence() { + VARIABLE_NAME=$1 + if [ -z ${!VARIABLE_NAME} ]; then + echo "${VARIABLE_NAME} is unset. Please set this label to run this docker container"; + exit 1 + fi +} + +assertPresence UWSGI_PROCESS_PER_CONTAINER +sed -i.bak "s/UWSGI_PROCESS_PER_CONTAINER/$UWSGI_PROCESS_PER_CONTAINER/" /uwsgi.ini + +exec "$@" + +python manage.py compilemessages + +/app/docker/scripts/postgres-alive +python manage.py migrate --noinput + +exec python manage.py runserver 0:8000 diff --git a/docker/scripts/postgres-alive b/docker/scripts/postgres-alive new file mode 100755 index 0000000000000000000000000000000000000000..e1b1d769f574c3bd3297bb209331fd087c5ce8fa --- /dev/null +++ b/docker/scripts/postgres-alive @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +function assertPresence() { + VARIABLE_NAME=$1 + if [ -z ${!VARIABLE_NAME} ]; then + echo "${VARIABLE_NAME} is unset. Please set this label to run this docker container"; + exit 1 + fi +} + +assertPresence DATABASE_HOST +assertPresence DATABASE_USER +assertPresence DATABASE_PASSWORD +assertPresence DATABASE_PORT + +STATE=NOT_RUN +WAIT=1 + +is_alive() { + WAIT=$((WAIT+1)) + if [[ $WAIT -le 180 ]]; then + DB_CON_OK=$(PGPASSWORD=$DATABASE_PASSWORD psql -h $DATABASE_HOST -U $DATABASE_USER -p $DATABASE_PORT -t -A -c "SELECT datname FROM pg_database WHERE datname='$DATABASE_NAME'" $DATABASE_NAME | grep -i $DATABASE_NAME) + if [[ $DB_CON_OK == "$DATABASE_NAME" ]]; then + STATE=RUN + fi + else + exit + fi +} + +while [ $STATE == "NOT_RUN" ]; do + is_alive + sleep 1 +done diff --git a/docker/scripts/run-docker b/docker/scripts/run-docker new file mode 100755 index 0000000000000000000000000000000000000000..be5d39635b4c6d0d44c581f8b34c06f02191d60f --- /dev/null +++ b/docker/scripts/run-docker @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -e +docker build -t shellvalier-developer -f Dockerfile . +docker-compose up diff --git a/requirements.txt b/requirements.txt index c72847238a7deeb16dc32f5d77f8addf2718dd33..b01fc60731f4752c18f00694b660570625d9dfa5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ certifi==2020.6.20 Django==3.1.5 django-crispy-forms==1.10.0 django-delayed-union==0.1.4 +django-extensions==3.1.5 future==0.18.2 infinity==1.4 Jinja2==2.10.3 diff --git a/shellvalier/environment.py b/shellvalier/environment.py new file mode 100644 index 0000000000000000000000000000000000000000..aa4006a9e5ba56f7a06d25f58e10120dd2b4c9ce --- /dev/null +++ b/shellvalier/environment.py @@ -0,0 +1,67 @@ +"""Utils for accessing process environment variables""" + +import os +from typing import ( + Any, + Callable, + List, + Optional, +) + +from django.core.exceptions import ImproperlyConfigured + + +NOT_SET = object() + + +def get_environment(name: str, mapper: Optional[Callable[[str], Any]] = None, default=NOT_SET) -> Any: + """Return a value with the given name from the process environment variables""" + possible_environ_names = [name] + for environ_name in possible_environ_names: + try: + value = os.environ[environ_name] + except KeyError: + continue + else: + break + else: + if default is NOT_SET: + raise ImproperlyConfigured('The {} environment variable is not present under names: {}'.format(name, ', '.join(possible_environ_names))) + else: + return default + mapper = mapper or str + return mapper(value) + + +def boolean_mapper(value: str) -> bool: + """Map a string representation on a boolean value to Python bool""" + if value.lower() in {'true', '1', 'yes'}: + return True + if value.lower() in {'false', '0', 'no'}: + return False + raise ImproperlyConfigured('Boolean mapper cannot map the {} value to bool'.format(value)) + + +def list_mapper_factory(delimiter: Optional[str] = None, item_mapper: Optional[Callable[[str], Any]] = None) -> Callable[[str], List[Any]]: + """Return a function that maps lists serialized to a string with a specified delimiter to a Python list + + :param delimiter: the item delimiter in the serialized list + :param item_mapper: the mapper for list elements + :return: the mapper function + """ + delimiter = delimiter or ',' + item_mapper = item_mapper or str + + def list_mapper(value: str) -> List[Any]: + """Map a list serialized to a string to a Python list + + Assumes an empty string represents an empty list, not a list containing an empty string. + + :param value: the list serialized to string + :return: the parsed list + """ + if value == '': + return [] + return [item_mapper(item) for item in value.split(delimiter)] + + return list_mapper diff --git a/shellvalier/settings.py b/shellvalier/settings.py index f17b5f27751cdcf37696edd7f2b0964dfcfc3a89..797463aca63bea7d6a5c7e0579df919c03a1f0da 100644 --- a/shellvalier/settings.py +++ b/shellvalier/settings.py @@ -12,6 +12,9 @@ https://docs.djangoproject.com/en/2.1/ref/settings/ import os +from .environment import get_environment, boolean_mapper, list_mapper_factory + + # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -20,10 +23,10 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'xxx' +SECRET_KEY = get_environment('SECRET_KEY') # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = get_environment('DEBUG', mapper=boolean_mapper) # for production ''' @@ -34,7 +37,7 @@ CSRF_COOKIE_SECURE = True X_FRAME_OPTIONS = 'DENY' ''' -ALLOWED_HOSTS = ['127.0.0.1'] +ALLOWED_HOSTS = get_environment('ALLOWED_HOSTS', mapper=list_mapper_factory()) # Application definition @@ -56,6 +59,7 @@ INSTALLED_APPS = [ 'dictionary_statistics.apps.DictionaryStatisticsConfig', 'download.apps.DownloadConfig', 'crispy_forms', + 'django_extensions', ] CRISPY_TEMPLATE_PACK = 'bootstrap4' @@ -99,11 +103,11 @@ WSGI_APPLICATION = 'shellvalier.wsgi.application' DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'shellvalier', - 'USER': 'shellvalier', - 'PASSWORD': 'shellvalier', - 'HOST': 'localhost', - 'PORT': '', + 'NAME': get_environment('DATABASE_NAME'), + 'USER': get_environment('DATABASE_USER'), + 'PASSWORD': get_environment('DATABASE_PASSWORD'), + 'HOST': get_environment('DATABASE_HOST'), + 'PORT': get_environment('DATABASE_PORT', mapper=int), }}