diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..3fc88db6c5f45585b338f6f1746dd797e05d9c95 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.git +.venv +.envrc +static_files/ +media_files/ +node_modules/ +dist/ +majak_uistyleguide/collectedstatic diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6e89260ba92adabe1cb90bb33d4dd86fec5af834..b46436dc8c7eb25323ba42143fc6f29290b13cc3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,11 @@ +stages: + - build + image: docker:20.10.9 variables: DOCKER_TLS_CERTDIR: "/certs" + IMAGE_TAG_APP: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG services: - docker:20.10.9-dind @@ -9,11 +13,9 @@ services: before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY -build: +build_app: stage: build script: - - VERSION=`cat VERSION` - - docker pull $CI_REGISTRY_IMAGE:latest || true - - docker build --cache-from $CI_REGISTRY_IMAGE:latest --tag $CI_REGISTRY_IMAGE:$VERSION --tag $CI_REGISTRY_IMAGE:latest . - - docker push $CI_REGISTRY_IMAGE:$VERSION - - docker push $CI_REGISTRY_IMAGE:latest + - docker pull $CI_REGISTRY_IMAGE:test || true + - docker build --cache-from $CI_REGISTRY_IMAGE:test -t $IMAGE_TAG_APP . + - docker push $IMAGE_TAG_APP diff --git a/Dockerfile b/Dockerfile index 6793d7710d4981bc82535d2b85cb2289dc820f23..838966ae6e5dab129f15a5badbb3c0ecfe079d00 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,36 +1,29 @@ -FROM python:3.11 +FROM node:21 + +RUN apt-get update \ + && apt-get install -y python3 python3-pip \ + && rm -rf /var/lib/apt/lists/* RUN mkdir /app WORKDIR /app -# Install NodeJS -ENV NODE_MAJOR=20 -RUN apt-get update -RUN apt-get install -y ca-certificates curl gnupg -RUN mkdir -p /etc/apt/keyrings -RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg -RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list -RUN apt-get update -RUN apt-get install -y nodejs -RUN rm -rf /var/lib/apt/lists/* +COPY requirements requirements/ +RUN pip3 install --break-system-packages -r requirements/base.txt -r requirements/prod.txt COPY . . -RUN pip install -r requirements/base.txt -RUN npm install -RUN npm run prod - -# Placeholder values so the static files collect -RUN DJANGO_ALLOWED_HOSTS=x \ - DJANGO_SECRET_KEY=x \ - python manage.py collectstatic --noinput --settings=majak_uistyleguide.settings.production - -RUN bash -c "adduser --disabled-login --quiet --gecos app app && \ - chmod -R o+r /app/ && \ - chmod o+x /app/run.sh" +RUN bash -c 'adduser --disabled-login --quiet --gecos app app && \ + chown -R app:app /app/ && \ + chmod o+x /app/run.sh' USER app +RUN npm i +RUN npm run prod + ENV DJANGO_SETTINGS_MODULE "majak_uistyleguide.settings.production" +# fake values for required env variables used to run collectstatic during build +RUN DJANGO_SECRET_KEY=x DATABASE_URL=postgres://x/x DJANGO_ALLOWED_HOSTS=x \ + python3 manage.py collectstatic EXPOSE 8000 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..e8b7f73c1fd30f3a9af1112694d0f4afba689984 --- /dev/null +++ b/Makefile @@ -0,0 +1,56 @@ +#!/usr/bin/make -f + +PYTHON = python +VENV = .venv +PORT = 8009 + +help: + @echo "Setup:" + @echo " venv Setup virtual environment" + @echo " install Install dependencies to venv" + @echo " install-hooks Install pre-commit hooks" + @echo " hooks Run pre-commit hooks manually" + @echo " upgrade Upgrade requirements" + @echo "" + @echo "Application:" + @echo " run Run the application on port ${PORT}" + @echo " shell Run Django shell" + @echo "" + @echo "Database:" + @echo " migrations Generate migrations" + @echo " migrate Run migrations" + @echo "" + +venv: .venv/bin/python +.venv/bin/python: + ${PYTHON} -m venv ${VENV} + +install: venv + ${VENV}/bin/pip install -r requirements/base.txt + +install-hooks: + pre-commit install --install-hooks + +hooks: + pre-commit run -a + +run: venv + ${VENV}/bin/python manage.py runserver ${PORT} + +shell: venv + ${VENV}/bin/python manage.py shell_plus + +migrations: venv + ${VENV}/bin/python manage.py makemigrations + +migrate: venv + ${VENV}/bin/python manage.py migrate + +upgrade: + (cd requirements && pip-compile -U base.in) + (cd requirements && pip-compile -U prod.in) + + +.PHONY: help venv install install-hooks hooks run shell upgrade migrations migrate + +# EOF diff --git a/majak_uistyleguide/settings/base.py b/majak_uistyleguide/settings/base.py index 02b7e98e58b75de417658c65892a0c5b7774a92e..8c363fc5a838ac5437937328e7a9fc2973b777a8 100644 --- a/majak_uistyleguide/settings/base.py +++ b/majak_uistyleguide/settings/base.py @@ -17,46 +17,41 @@ WSGI_APPLICATION = "majak_uistyleguide.wsgi.application" # I18N and L10N # ------------------------------------------------------------------------------ -LANGUAGE_CODE = 'cs' +LANGUAGE_CODE = "cs" TIME_ZONE = "Europe/Prague" USE_I18N = True USE_TZ = True # DATABASES # ------------------------------------------------------------------------------ -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ROOT_DIR / 'db.sqlite3', - } -} -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DATABASES = {"default": env.db("DATABASE_URL")} +DATABASES["default"]["ATOMIC_REQUESTS"] = True +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" # APPS # ------------------------------------------------------------------------------ INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - - 'django_vite', - 'pattern_library', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django_vite", + "pattern_library", ] # MIDDLEWARE # ------------------------------------------------------------------------------ MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', + "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", - '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', + "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", ] # TEMPLATES @@ -78,7 +73,7 @@ TEMPLATES = [ ], "builtins": [ "pattern_library.loader_tags", - "majak_uistyleguide.templatetags.math" + "majak_uistyleguide.templatetags.math", ], }, }, @@ -102,8 +97,8 @@ MEDIA_ROOT = str(ROOT_DIR / "media_files") # VITE SETTINGS # ------------------------------------------------------------------------------ # Where ViteJS assets are built. -DJANGO_VITE_ASSETS_PATH = ROOT_DIR / 'dist' -STATIC_FILES = PROJECT_DIR / 'static' +DJANGO_VITE_ASSETS_PATH = ROOT_DIR / "dist" +STATIC_FILES = PROJECT_DIR / "static" # If use HMR or not. DJANGO_VITE_DEV_MODE = False @@ -113,7 +108,7 @@ STATIC_ROOT = PROJECT_DIR / "collectedstatic" # Include DJANGO_VITE_ASSETS_PATH into STATICFILES_DIRS to be copied inside # when run command python manage.py collectstatic -SRC_PATH = ROOT_DIR / 'src' +SRC_PATH = ROOT_DIR / "src" STATICFILES_DIRS = [DJANGO_VITE_ASSETS_PATH, STATIC_FILES, SRC_PATH] # PATTERN LIBRARY SETTINGS @@ -131,14 +126,11 @@ PATTERN_LIBRARY = { ("organisms", ["patterns/organisms"]), ("templates", ["patterns/templates"]), ), - # Configure which files to detect as templates. "TEMPLATE_SUFFIX": ".html", - # Set which template components should be rendered inside of, # so they may use page-level component dependencies like CSS. "PATTERN_BASE_TEMPLATE_NAME": "patterns/base.html", - # Any template in BASE_TEMPLATE_NAMES or any template that extends a template in # BASE_TEMPLATE_NAMES is a "page" and will be rendered as-is without being wrapped. "BASE_TEMPLATE_NAMES": ["patterns/base_page.html"], diff --git a/majak_uistyleguide/settings/production.py b/majak_uistyleguide/settings/production.py index d434da474ee7278ecec445d6dadd4841eed1ada3..6cfbf22c459b0814cb8c93ec273e3de80fe14dfb 100644 --- a/majak_uistyleguide/settings/production.py +++ b/majak_uistyleguide/settings/production.py @@ -1,6 +1,10 @@ from .base import * from .base import env +# DATABASES +# ------------------------------------------------------------------------------ +DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60) + # SECURITY # ------------------------------------------------------------------------------ ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS") diff --git a/majak_uistyleguide/wsgi.py b/majak_uistyleguide/wsgi.py index 917dbfdb37bd284dab875ac7fc5a817973e7ff9e..aa45fe517c8b1afdf3115bd322ad24ba011fd2bf 100644 --- a/majak_uistyleguide/wsgi.py +++ b/majak_uistyleguide/wsgi.py @@ -11,6 +11,6 @@ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'majak_uistyleguide.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "majak_uistyleguide.settings.dev") application = get_wsgi_application() diff --git a/manage.py b/manage.py index 902083c31403355dbb8ba41d98e77ca3886c0c51..494ee951fe02fc049d7ded8b1d8258e2cab5cef3 100755 --- a/manage.py +++ b/manage.py @@ -1,22 +1,15 @@ #!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" import os import sys def main(): - """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'majak_uistyleguide.settings') - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - 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?" - ) from exc + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "majak_uistyleguide.settings.dev") + + from django.core.management import execute_from_command_line + execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/requirements/base.in b/requirements/base.in new file mode 100644 index 0000000000000000000000000000000000000000..1e8edefd1ed6150fa2fb0736a0c00f5db148531f --- /dev/null +++ b/requirements/base.in @@ -0,0 +1,6 @@ +django<4 +django-pattern-library +django-environ +django-vite +psycopg2-binary +whitenoise diff --git a/requirements/base.txt b/requirements/base.txt index b1925eb0f8890c88caac10fdeab9987917f0a493..08ad028ac9cac4d90a4b503d3ad01c568596c946 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,6 +1,33 @@ -django==4.0 -django-pattern-library==1.0.0 -django-environ==0.9.0 -django-vite==2.0.2 -gunicorn==21.2.0 +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile base.in +# +asgiref==3.7.2 + # via django +django==3.2.24 + # via + # -r base.in + # django-pattern-library + # django-vite +django-environ==0.11.2 + # via -r base.in +django-pattern-library==1.2.0 + # via -r base.in +django-vite==3.0.3 + # via -r base.in +markdown==3.5.2 + # via django-pattern-library +psycopg2-binary==2.9.9 + # via -r base.in +pytz==2024.1 + # via django +pyyaml==6.0.1 + # via django-pattern-library +sqlparse==0.4.4 + # via django +typing-extensions==4.9.0 + # via asgiref whitenoise==6.6.0 + # via -r base.in diff --git a/requirements/prod.in b/requirements/prod.in new file mode 100644 index 0000000000000000000000000000000000000000..8f22dccf99affb5ab9b1c65023ec083269269bca --- /dev/null +++ b/requirements/prod.in @@ -0,0 +1 @@ +gunicorn diff --git a/requirements/prod.txt b/requirements/prod.txt new file mode 100644 index 0000000000000000000000000000000000000000..f4a88f12b4c6224900fea3972bc4e461fa4b7a51 --- /dev/null +++ b/requirements/prod.txt @@ -0,0 +1,10 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile prod.in +# +gunicorn==21.2.0 + # via -r prod.in +packaging==23.2 + # via gunicorn diff --git a/run.sh b/run.sh index 44ce2f937a524e71decc18ee9ea9c8ba7b5d1b5d..e51a2615de12304e43e2df0f1a7c190f446dbb41 100644 --- a/run.sh +++ b/run.sh @@ -4,7 +4,7 @@ set -e # migrate database -python manage.py migrate +python3 manage.py migrate # start webserver exec gunicorn -c gunicorn.conf.py majak_uistyleguide.wsgi