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

Initial commit

parents
No related branches found
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