Skip to content
Snippets Groups Projects
Commit 0d9ecc08 authored by jan.bednarik's avatar jan.bednarik
Browse files

Initial commit

parents
Branches
No related tags found
No related merge requests found
Pipeline #14670 failed
Showing with 871 additions and 0 deletions
.git
.venv
.envrc
static_files/
media_files/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
#####################################################
# CUSTOM
# direnv
.envrc
media_files/
static_files/
.python-version
.idea/
stages:
- build
image: docker:20.10.8
variables:
DOCKER_TLS_CERTDIR: "/certs"
IMAGE_TAG_APP: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
services:
- docker:20.10.8-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
build_app:
stage: build
script:
- docker build -t $IMAGE_TAG_APP .
- docker push $IMAGE_TAG_APP
[settings]
# config compatible with Black
line_length = 88
multi_line_output = 3
include_trailing_comma = true
known_third_party = asgiref,dateutil,django,environ,glom,httpx,pirates,sentry_sdk
default_language_version:
python: python3.10
exclude: snapshots/
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
exclude: ^.*\.md$
- id: end-of-file-fixer
- id: debug-statements
- id: mixed-line-ending
args: [--fix=lf]
- id: detect-private-key
- id: check-merge-conflict
- repo: https://github.com/asottile/seed-isort-config
rev: v2.2.0
hooks:
- id: seed-isort-config
- repo: https://github.com/timothycrosley/isort
rev: 5.12.0
hooks:
- id: isort
- repo: https://github.com/psf/black
rev: 23.7.0
hooks:
- id: black
[MASTER]
# jobs=0 means 'use all CPUs'
jobs=0
[MESSAGES CONTROL]
disable =
missing-docstring,
line-too-long,
invalid-name,
no-value-for-parameter,
no-member,
unused-argument,
broad-except,
relative-import,
wrong-import-position,
bare-except,
locally-disabled,
protected-access,
abstract-method,
no-self-use,
fixme,
too-few-public-methods,
ungrouped-imports,
bad-continuation,
redefined-outer-name,
too-many-ancestors,
attribute-defined-outside-init,
too-many-arguments,
[REPORTS]
output-format=colorized
[FORMAT]
logging-modules=
logging,
structlog,
FROM python:3.10
RUN apt-get update && apt-get install -y \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir /app
WORKDIR /app
COPY requirements requirements/
RUN pip install -r requirements/base.txt -r requirements/prod.txt
COPY . .
RUN bash -c 'adduser --disabled-login --quiet --gecos app app && \
chmod -R o+r /app/ && \
mkdir /app/media_files && \
mkdir /app/static_files && \
chown -R app:app /app/media_files && \
chown -R app:app /app/static_files && \
chmod o+x /app/run.sh'
USER app
ENV DJANGO_SETTINGS_MODULE "alligator.settings.prod"
# 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 \
OIDC_RP_CLIENT_ID=x OIDC_RP_CLIENT_SECRET=x OIDC_RP_REALM_URL=x \
python manage.py collectstatic
EXPOSE 8000
CMD ["bash", "run.sh"]
Makefile 0 → 100644
#!/usr/bin/make -f
PYTHON = python
VENV = .venv
PORT = 8017
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 ""
@echo "Testing:"
@echo " test Run tests"
@echo " coverage Coverage report"
@echo ""
venv: .venv/bin/python
.venv/bin/python:
${PYTHON} -m venv ${VENV}
install: venv
${VENV}/bin/pip install -r requirements/base.txt -r requirements/dev.txt
install-hooks:
pre-commit install --install-hooks
hooks:
pre-commit run -a
run: venv
DJANGO_SETTINGS_MODULE=alligator.settings.dev ${VENV}/bin/uvicorn --port ${PORT} --reload alligator.asgi:application
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
test:
${VENV}/bin/pytest -n auto
coverage:
${VENV}/bin/pytest -n auto --cov --cov-report term-missing
upgrade:
(cd requirements && pip-compile -U base.in)
(cd requirements && pip-compile -U prod.in)
(cd requirements && pip-compile -U dev.in)
.PHONY: help venv install install-hooks hooks run shell upgrade
.PHONY: migrations migrate test coverage
# EOF
README.md 0 → 100644
# Alligator
_Aligátor sežral Pelikána... R.I.P._
Pirátská SMS brána.
[![code style: Black](https://img.shields.io/badge/code%20style-Black-000000)](https://github.com/psf/black)
[![powered by: Django](https://img.shields.io/badge/powered%20by-Django-0C4B33)](https://www.djangoproject.com)
## Pod pokličkou
### Struktura projektu
.
├── alligator = Django projekt s konfigurací
└── users = app s custom user modelem a SSO, apod.
## Deployment
### Konfigurace
Je třeba nastavit environment proměnné:
| proměnná | default | popis |
| --- | --- | --- |
| `DATABASE_URL` | | DSN k databázi (např. `postgres://user:pass@localhost:5342/alligator`) |
| `OIDC_RP_REALM_URL` | | OpenID server realm URL (např. `http://localhost:8080/realms/master/`) |
| `OIDC_RP_CLIENT_ID` | | OpenID Client ID |
| `OIDC_RP_CLIENT_SECRET` | | OpenID Client Secret |
V produkci musí být navíc nastaveno:
| proměnná | default | popis |
| --- | --- | --- |
| `DJANGO_SECRET_KEY` | | tajný šifrovací klíč |
| `DJANGO_ALLOWED_HOSTS` | | allowed hosts (více hodnot odděleno čárkami) |
| `DJANGO_SETTINGS_MODULE` | `alligator.settings.prod` | produkční settings |
Různé:
| proměnná | default | popis |
| --- | --- | --- |
| `SENTRY_DSN` | | pokud je zadáno, pády se reportují do Sentry |
### Management commands
Přes CRON je třeba na pozadí spouštět Django `manage.py` commandy:
* `clearsessions` - maže expirované sessions (denně až týdně)
## Vývoj
Pro vývoj je definován pomocný `Makefile` pro časté akce. Pro nápovědu zavolej:
$ make help
### Lokální instalace a spuštění
#### Vytvoření virtualenv pro instalaci závislostí
Vytvoř virtualenv:
$ make venv
Vytvoří virtualenv ve složce `.venv`. Předpokládá že výchozí `python` v terminálu
je Python 3. Pokud tomu tak není, použijte třeba [Pyenv](https://github.com/pyenv/pyenv)
pro instalaci více verzí Pythonu bez rizika rozbití systému.
#### Aktivace virtualenvu
Před prací na projektu je třeba aktivovat virtualenv. To bohužel nejde dělat
pomocí nástroje `make`. Je třeba zavolat příkaz:
$ source .venv/bin/activate
Můžete asi na to vytvořit alias pro shell. Do `~/.bash_profile` nebo `~/.zshrc`
nebo jiného konfiguračního souboru dle vašeho shellu přidejte:
alias senv='source .venv/bin/activate'
A pak můžete virtualenv aktivovat pouze jednoduchým voláním:
$ senv
Pro sofistikovanější řešení, které vám aktivuje virtualenv při změně adresáře na
adresář s projektem, slouží nástroj [direnv](https://direnv.net/).
Deaktivace virtualenvu se dělá příkazem:
$ deactivate
#### Instalace závislostí
V aktivovaném virtualenvu spusťte:
$ make install
To nainstaluje Pythonní závislosti pro vývoj projektu na lokále.
#### Nastavení environment proměnných
Environment proměnné (viz konfigurace výše) se načítají ze souboru `.env`, který
může vypadat takto:
DATABASE_URL=postgres://db:db@localhost:5432/alligator
OIDC_RP_REALM_URL=http://localhost:8080/realms/master/
OIDC_RP_CLIENT_ID=alligator
OIDC_RP_CLIENT_SECRET=abcd
Pro lokální vývoj obsahují settings tyto výchozí hodnoty:
DEBUG = True
ALLOWED_HOSTS = ["*"]
### Management projektu
#### Migrace databáze
Aplikuj migrace databáze:
$ make migrate
Při změně modelů vygeneruj migrace pomocí:
$ make migrations
#### Spuštění development serveru
Django development server na portu `8017` se spustí příkazem:
$ make run
Poté můžete otevřít web na adrese [http://localhost:8017](http://localhost:8017)
#### Django shell
Django shell používající `shell_plus` z Django extensions spustíte:
$ make shell
### Testy
Používá se testovací framework [pytest](https://pytest.org). Spuštění testů:
$ pytest
Případně přes `make`, ale bez možnosti parametrizovat spuštění testů:
$ make test
Coverage report:
$ make coverage
### Code quality
K formátování kódu se používá [black](https://github.com/psf/black). Doporučujeme
ho nainstalovat do vašeho editoru kódu, aby soubory přeformátoval po uložení.
Přeformátování kódu nástrojem `black` je součástí `pre-commit` hooks (viz níže).
Součástí `pre-commit` hooků je také automatické seřazení importů v Pythonních
souborech nástrojem [isort](https://github.com/timothycrosley/isort/).
### Pre-commit hooky
Použivá se [pre-commit](https://pre-commit.com/) framework pro management git
pre-commit hooks.
Máte-li pre-commit framework [nainstalovaný](https://pre-commit.com/#installation)
spusttě příkaz:
$ make install-hooks
Ten naisntaluje hooky pro projekt. A poté při každém commitu dojde k požadovaným
akcím na změněných souborech.
Ručně se dají hooky na všechny soubory spustit příkazem:
$ make hooks
## Upgrade závislostí
K upgrade se používají [pip-tools](https://github.com/jazzband/pip-tools) (`pip install pip-tools`):
$ cd requirements/
$ pip-compile -U base.in
$ pip-compile -U prod.in
$ pip-compile -U dev.in
Tím se vygenerují `base.txt`, `prod.txt` a `dev.txt`.
Nebo to stejné lze provést příkazem:
$ make upgrade
"""
ASGI config for Alligator 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/4.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "alligator.settings.prod")
application = get_asgi_application()
import logging
from os.path import join
from pathlib import Path
import environ
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.logging import LoggingIntegration
ROOT_DIR = Path(__file__).parents[2]
PROJECT_DIR = Path(__file__).parents[1]
env = environ.Env()
environ.Env.read_env(str(ROOT_DIR / ".env"))
# GENERAL
# ------------------------------------------------------------------------------
DEBUG = env.bool("DJANGO_DEBUG", False)
ROOT_URLCONF = "alligator.urls"
WSGI_APPLICATION = None
# I18N and L10N
# ------------------------------------------------------------------------------
TIME_ZONE = "Europe/Prague"
LANGUAGE_CODE = "cs"
USE_I18N = True
USE_L10N = True
USE_TZ = True
# DATABASES
# ------------------------------------------------------------------------------
DATABASES = {"default": env.db("DATABASE_URL")}
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# APPS
# ------------------------------------------------------------------------------
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.humanize",
"django.contrib.postgres",
"django.forms",
"django_extensions",
"pirates",
"users",
]
# AUTHENTICATION
# ------------------------------------------------------------------------------
AUTHENTICATION_BACKENDS = ["pirates.auth.PiratesOIDCAuthenticationBackend"]
AUTH_USER_MODEL = "users.User"
LOGIN_REDIRECT_URL = "/admin"
LOGOUT_REDIRECT_URL = "/admin"
LOGIN_URL = "/admin"
OIDC_RP_CLIENT_ID = env.str("OIDC_RP_CLIENT_ID")
OIDC_RP_CLIENT_SECRET = env.str("OIDC_RP_CLIENT_SECRET")
OIDC_RP_REALM_URL = env.str("OIDC_RP_REALM_URL")
OIDC_RP_SIGN_ALGO = "RS256"
OIDC_OP_JWKS_ENDPOINT = join(OIDC_RP_REALM_URL, "protocol/openid-connect/certs")
OIDC_OP_AUTHORIZATION_ENDPOINT = join(OIDC_RP_REALM_URL, "protocol/openid-connect/auth")
OIDC_OP_TOKEN_ENDPOINT = join(OIDC_RP_REALM_URL, "protocol/openid-connect/token")
OIDC_OP_USER_ENDPOINT = join(OIDC_RP_REALM_URL, "protocol/openid-connect/userinfo")
# MIDDLEWARE
# ------------------------------------------------------------------------------
MIDDLEWARE = [
"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.middleware.security.SecurityMiddleware",
]
# STATIC
# ------------------------------------------------------------------------------
STATIC_ROOT = str(ROOT_DIR / "static_files")
STATIC_URL = "/static/"
STATICFILES_FINDERS = [
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
]
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage"
# MEDIA
# ------------------------------------------------------------------------------
MEDIA_URL = "/media/"
MEDIA_ROOT = str(ROOT_DIR / "media_files")
# TEMPLATES
# ------------------------------------------------------------------------------
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [str(PROJECT_DIR / "templates")],
"OPTIONS": {
"loaders": [
"django.template.loaders.filesystem.Loader",
"django.template.loaders.app_directories.Loader",
],
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
# SECURITY
# ------------------------------------------------------------------------------
SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_HTTPONLY = True
SECURE_BROWSER_XSS_FILTER = True
X_FRAME_OPTIONS = "SAMEORIGIN"
# EMAIL
# ------------------------------------------------------------------------------
EMAIL_CONFIG = env.email("EMAIL_URL", default="consolemail://")
vars().update(EMAIL_CONFIG)
# LOGGING
# ------------------------------------------------------------------------------
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s"
}
},
"handlers": {
"console": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "verbose",
}
},
"root": {"level": "INFO", "handlers": ["console"]},
}
# CACHES
# ------------------------------------------------------------------------------
CACHES = {"default": env.cache("CACHE_URL", default="locmemcache://")}
CACHES["default"]["TIMEOUT"] = 60 * 60 * 24
# SENTRY
# ------------------------------------------------------------------------------
SENTRY_DSN = env.str("SENTRY_DSN", default="")
if SENTRY_DSN:
sentry_sdk.init(
dsn=SENTRY_DSN,
integrations=[
DjangoIntegration(),
LoggingIntegration(level=logging.INFO, event_level=logging.WARNING),
],
send_default_pii=True,
)
# CUSTOM SETTINGS
# ------------------------------------------------------------------------------
PORTAINER_API_URL = env.str("PORTAINER_API_URL")
PORTAINER_TOKEN = env.str("PORTAINER_TOKEN")
PORTAINER_ENVIRONMENT_ID = 2
from .base import *
from .base import env
# GENERAL
# ------------------------------------------------------------------------------
DEBUG = env.bool("DJANGO_DEBUG", default=True)
SECRET_KEY = env("DJANGO_SECRET_KEY", default="58as*987asd4omcvJKLDShj")
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["*"])
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")
SECRET_KEY = env("DJANGO_SECRET_KEY")
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# set this to 60 seconds first and then to 518400 once you prove the former works
SECURE_HSTS_SECONDS = 518400
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_CONTENT_TYPE_NOSNIFF = True
# STATIC
# ------------------------------------------------------------------------------
MIDDLEWARE.insert(1, "whitenoise.middleware.WhiteNoiseMiddleware")
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
# LOGGING
# ------------------------------------------------------------------------------
LOGGING["filters"] = {
"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}
}
{% extends "admin/base_site.html" %}
{% load static %}
{% block extrahead %}
<link rel="apple-touch-icon" sizes="180x180" href="{% static "icon/apple-touch-icon.png" %}">
<link rel="icon" type="image/png" sizes="32x32" href="{% static "icon/favicon-32x32.png" %}">
<link rel="icon" type="image/png" sizes="16x16" href="{% static "icon/favicon-16x16.png" %}">
{% endblock %}
{% extends "admin/base_site.html" %}
{% load i18n static %}
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" href="{% static "admin/css/login.css" %}">
{{ form.media }}
{% endblock %}
{% block bodyclass %}{{ block.super }} login{% endblock %}
{% block usertools %}{% endblock %}
{% block nav-global %}{% endblock %}
{% block nav-sidebar %}{% endblock %}
{% block content_title %}{% endblock %}
{% block nav-breadcrumbs %}{% endblock %}
{% block content %}
{% if form.errors and not form.non_field_errors %}
<p class="errornote">
{% blocktranslate count counter=form.errors.items|length %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktranslate %}
</p>
{% endif %}
{% if form.non_field_errors %}
{% for error in form.non_field_errors %}
<p class="errornote">
{{ error }}
</p>
{% endfor %}
{% endif %}
<div id="content-main">
{% if user.is_authenticated %}
<p class="errornote">
{% blocktranslate trimmed %}
You are authenticated as {{ username }}, but are not authorized to
access this page. Would you like to login to a different account?
{% endblocktranslate %}
</p>
{% endif %}
<p style="text-align: center; padding-top: 10px;">
<a class="button" style="padding: 10px 15px;" href="{% url 'oidc_authentication_init' %}?next={{ request.GET.next }}">
Přihlásit se Pirátskou identitou
</a>
</p>
</div>
{% endblock %}
from django.conf import settings
from django.contrib import admin
from django.urls import include, path
from django.views.generic.base import RedirectView
from pirates.urls import urlpatterns as pirates_urlpatterns
import portainer.urls
urlpatterns = [
path("admin/", admin.site.urls),
path("portainer/", include(portainer.urls)),
path("", RedirectView.as_view(url="/admin/")),
] + pirates_urlpatterns
if settings.DEBUG:
from django.conf.urls.static import static
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
# Serve static and media files from development server
urlpatterns += staticfiles_urlpatterns()
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
bind = "0.0.0.0:8000"
accesslog = "-"
workers = 1
max_requests = 1000
max_requests_jitter = 10
timeout = 60
graceful_timeout = 60
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "alligator.settings.dev")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment