From ff710ef987e1d706ac75855f50e159a89e541864 Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Fri, 10 Mar 2023 07:58:12 +0100 Subject: [PATCH 1/5] Deployment with docker compose #376 --- .github/workflows/docker-build-push.yml | 2 + Dockerfile | 5 +- deployment/docker-compose/.env.example | 26 ++++++ deployment/docker-compose/README.md | 23 ++++++ deployment/docker-compose/docker-compose.yml | 82 +++++++++++++++++++ .../docker-compose/fluentd-custom.config | 60 ++++++++++++++ deployment/docker-compose/init.sh | 10 +++ deployment/docker-compose/update.sh | 6 ++ doc/development.md | 2 +- project/__init__.py | 11 ++- project/views/root.py | 8 +- 11 files changed, 228 insertions(+), 7 deletions(-) create mode 100644 deployment/docker-compose/.env.example create mode 100644 deployment/docker-compose/README.md create mode 100644 deployment/docker-compose/docker-compose.yml create mode 100644 deployment/docker-compose/fluentd-custom.config create mode 100755 deployment/docker-compose/init.sh create mode 100755 deployment/docker-compose/update.sh diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 4fc2db8..746ae75 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -2,6 +2,8 @@ name: Docker build and push on: push: + branches: + - 'main' tags: - 'v*' workflow_dispatch: diff --git a/Dockerfile b/Dockerfile index e7d2754..48d8ad7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ -FROM python:3.7-slim-buster +FROM python:3.7 # Add rsync RUN apt update -qq && apt upgrade -y && apt autoremove -y -RUN apt install -y rsync && apt autoremove -y +RUN apt install -y rsync curl && apt autoremove -y EXPOSE 5000 @@ -24,6 +24,7 @@ ENV STATIC_FILES_MIRROR="" # Install pip requirements COPY requirements.txt . +RUN python -m pip install --upgrade pip RUN python -m pip install -r requirements.txt WORKDIR /app diff --git a/deployment/docker-compose/.env.example b/deployment/docker-compose/.env.example new file mode 100644 index 0000000..bd16dbe --- /dev/null +++ b/deployment/docker-compose/.env.example @@ -0,0 +1,26 @@ +POSTGRES_DATA_PATH=./tmp/data/postgres/data +POSTGRES_BACKUP_PATH=./tmp/data/postgres/backups +CACHE_PATH=./tmp/cache +STATIC_PATH=./tmp/static +FLUENTD_LOG_PATH=./tmp/logs/fluentd +FLUENTD_CUSTOM_CONFIG_PATH=./tmp/config +FLUENTD_DOCKER_CONTAINERS_PATH=/var/lib/docker/containers + +POSTGRES_USER=oveda +POSTGRES_PASSWORD= +POSTGRES_DB=oveda + +WEB_TAG=latest +SERVER_NAME= +PREFERRED_URL_SCHEME=https +SECRET_KEY= +SECURITY_PASSWORD_HASH= +MAIL_SERVER= +MAIL_PORT= +MAIL_USERNAME= +MAIL_PASSWORD= +MAIL_DEFAULT_SENDER= +MAIL_USE_TLS=True +GOOGLE_MAPS_API_KEY=AIzaDummy +JWT_PRIVATE_KEY="" +JWT_PUBLIC_JWKS='' \ No newline at end of file diff --git a/deployment/docker-compose/README.md b/deployment/docker-compose/README.md new file mode 100644 index 0000000..9fcc683 --- /dev/null +++ b/deployment/docker-compose/README.md @@ -0,0 +1,23 @@ +# Deployment with Docker compose + +## Configure + +Copy example.env to .env and enter values. + +## Initialize + +```sh +./init.sh +``` + +## Start + +```sh +docker compose up --force-recreate --detach +``` + +## Update app + +```sh +./update.sh +``` diff --git a/deployment/docker-compose/docker-compose.yml b/deployment/docker-compose/docker-compose.yml new file mode 100644 index 0000000..9dba2e9 --- /dev/null +++ b/deployment/docker-compose/docker-compose.yml @@ -0,0 +1,82 @@ +version: "3.9" +name: "oveda" + +services: + db: + image: postgis/postgis:12-3.1 + restart: always + healthcheck: + test: "pg_isready --username=${POSTGRES_USER} && psql --username=${POSTGRES_USER} --list" + start_period: "5s" + ports: + - 5434:5432 + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + volumes: + - ${POSTGRES_DATA_PATH}:/var/lib/postgresql/data + + db-backup: + image: prodrigestivill/postgres-backup-local:12 + restart: always + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_HOST: db + POSTGRES_EXTRA_OPTS: "-Z6 -c" + SCHEDULE: "0 0 22 * * *" + volumes: + - ${POSTGRES_BACKUP_PATH}:/backups + depends_on: + db: + condition: service_healthy + + web: + image: danielgrams/gsevpt:${WEB_TAG} + restart: always + healthcheck: + test: "curl -f ${SERVER_NAME}/up" + interval: "60s" + timeout: "5s" + start_period: "5s" + ports: + - "5000:5000" + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + FLASK_APP: main.py + DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db/${POSTGRES_DB} + SECRET_KEY: ${SECRET_KEY} + SECURITY_PASSWORD_HASH: ${SECURITY_PASSWORD_HASH} + MAIL_DEFAULT_SENDER: ${MAIL_DEFAULT_SENDER} + MAIL_PASSWORD: ${MAIL_PASSWORD} + MAIL_PORT: ${MAIL_PORT} + MAIL_SERVER: ${MAIL_SERVER} + MAIL_USE_TLS: ${MAIL_USE_TLS} + MAIL_USERNAME: ${MAIL_USERNAME} + GOOGLE_MAPS_API_KEY: ${GOOGLE_MAPS_API_KEY} + SERVER_NAME: ${SERVER_NAME} + PREFERRED_URL_SCHEME: ${PREFERRED_URL_SCHEME} + GUNICORN_ACCESS_LOG: "-" + STATIC_FILES_MIRROR: /static + CACHE_PATH: tmp + JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY} + JWT_PUBLIC_JWKS: ${JWT_PUBLIC_JWKS} + volumes: + - ${CACHE_PATH}:/app/project/tmp + - ${STATIC_PATH}:/static + depends_on: + db: + condition: service_healthy + + fluentd: + image: danielgrams/fluentd + restart: always + environment: + FLUENTD_CONF: fluentd-custom.config + volumes: + - ${FLUENTD_LOG_PATH}:/fluentd/log + - ${FLUENTD_CUSTOM_CONFIG_PATH}/fluentd-custom.config:/fluentd/etc/fluentd-custom.config + - ${FLUENTD_DOCKER_CONTAINERS_PATH}:/fluentd/containers diff --git a/deployment/docker-compose/fluentd-custom.config b/deployment/docker-compose/fluentd-custom.config new file mode 100644 index 0000000..d09bf34 --- /dev/null +++ b/deployment/docker-compose/fluentd-custom.config @@ -0,0 +1,60 @@ +# Tail docker logs + + @type tail + read_from_head true + path /fluentd/containers/*/*-json.log + pos_file /fluentd/log/docker.pos + time_format %Y-%m-%dT%H:%M:%S + tag docker.* + format json + + +# Add container id + + @type record_transformer + + container_id ${tag_parts[3]} + + + +# Errors only + + @type grep + + key log + pattern /\[error\]|\| error \|/i + + + + + @type copy + + # Write errors to file + + @type file + path /fluentd/log/docker-error + + + # Tag daily errors + + @type grepcounter + count_interval 14400 # = 4 hours + input_key log + threshold 1 + add_tag_prefix daily_error + + + +# Send daily error mail +# +# @type mail +# host [Host] +# port [Port] +# user [User/Email] +# password [Password] +# from [User/Email] +# to [Recipient] +# subject 'Error alert fluentd' +# message Error occured %s times +# message_out_keys count +# \ No newline at end of file diff --git a/deployment/docker-compose/init.sh b/deployment/docker-compose/init.sh new file mode 100755 index 0000000..71a3ee4 --- /dev/null +++ b/deployment/docker-compose/init.sh @@ -0,0 +1,10 @@ +set -e +source .env + +mkdir -p ${POSTGRES_DATA_PATH} +mkdir -p ${POSTGRES_BACKUP_PATH} +mkdir -p ${CACHE_PATH} +mkdir -p ${STATIC_PATH} +mkdir -p ${FLUENTD_LOG_PATH} +mkdir -p ${FLUENTD_CUSTOM_CONFIG_PATH} +cp ./fluentd-custom.config ${FLUENTD_CUSTOM_CONFIG_PATH}/fluentd-custom.config \ No newline at end of file diff --git a/deployment/docker-compose/update.sh b/deployment/docker-compose/update.sh new file mode 100755 index 0000000..32f7d80 --- /dev/null +++ b/deployment/docker-compose/update.sh @@ -0,0 +1,6 @@ +set -e + +docker compose pull web +docker compose stop web +docker compose run db-backup /backup.sh +docker compose up --detach --force-recreate web \ No newline at end of file diff --git a/doc/development.md b/doc/development.md index 7e195dd..5a2eac4 100644 --- a/doc/development.md +++ b/doc/development.md @@ -68,7 +68,7 @@ pybabel compile -d project/translations ### Build image ```sh -docker build -t gsevpt:latest . +docker build -t danielgrams/gsevpt:latest . ``` ### Run container with existing postgres server diff --git a/project/__init__.py b/project/__init__.py index 763986c..1b2c79a 100644 --- a/project/__init__.py +++ b/project/__init__.py @@ -14,6 +14,11 @@ from flask_wtf.csrf import CSRFProtect from project.custom_session_interface import CustomSessionInterface + +def getenv_bool(name: str, default: str = "False"): + return os.getenv(name, default).lower() in ("true", "1", "t") + + # Create app app = Flask(__name__) app.config["SQLALCHEMY_DATABASE_URI"] = os.environ["DATABASE_URL"] @@ -95,15 +100,15 @@ app.config["WTF_CSRF_CHECK_DEFAULT"] = False # Mail mail_server = os.getenv("MAIL_SERVER") -if mail_server is None: +if not mail_server: app.config["MAIL_SUPPRESS_SEND"] = True app.config["MAIL_DEFAULT_SENDER"] = "test@oveda.de" else: # pragma: no cover app.config["MAIL_SUPPRESS_SEND"] = False app.config["MAIL_SERVER"] = mail_server app.config["MAIL_PORT"] = os.getenv("MAIL_PORT") - app.config["MAIL_USE_TLS"] = os.getenv("MAIL_USE_TLS", True) - app.config["MAIL_USE_SSL"] = os.getenv("MAIL_USE_SSL", False) + app.config["MAIL_USE_TLS"] = getenv_bool("MAIL_USE_TLS", "True") + app.config["MAIL_USE_SSL"] = getenv_bool("MAIL_USE_SSL", "False") app.config["MAIL_USERNAME"] = os.getenv("MAIL_USERNAME") app.config["MAIL_PASSWORD"] = os.getenv("MAIL_PASSWORD") app.config["MAIL_DEFAULT_SENDER"] = os.getenv("MAIL_DEFAULT_SENDER") diff --git a/project/views/root.py b/project/views/root.py index 4dd80a1..5289e3a 100644 --- a/project/views/root.py +++ b/project/views/root.py @@ -5,7 +5,7 @@ from flask import redirect, render_template, request, send_from_directory, url_f from flask_babelex import gettext from markupsafe import Markup -from project import app, cache_path, dump_path, robots_txt_file, sitemap_file +from project import app, cache_path, db, dump_path, robots_txt_file, sitemap_file from project.services.admin import upsert_settings from project.views.utils import track_analytics @@ -32,6 +32,12 @@ def home(): ) +@app.route("/up") +def up(): + db.engine.execute("SELECT 1") + return "OK" + + @app.route("/tos") def tos(): title = gettext("Terms of service") From 61f3f5dbaf0569ff679a51ff57b9edf8b9292d4d Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Fri, 10 Mar 2023 08:03:50 +0100 Subject: [PATCH 2/5] Deployment with docker compose #376 --- migrations/versions/091deace5f08_.py | 1 - project/services/importer/ld_json_importer.py | 1 - project/views/event.py | 1 - requirements.txt | 14 ++++++++------ tests/services/importer/test_event_importer.py | 1 - 5 files changed, 8 insertions(+), 10 deletions(-) diff --git a/migrations/versions/091deace5f08_.py b/migrations/versions/091deace5f08_.py index 7e08304..00907ae 100644 --- a/migrations/versions/091deace5f08_.py +++ b/migrations/versions/091deace5f08_.py @@ -18,7 +18,6 @@ depends_on = None def upgrade(): - bind = op.get_bind() bind.execute(text("create extension if not exists postgis;")) diff --git a/project/services/importer/ld_json_importer.py b/project/services/importer/ld_json_importer.py index 131d6b1..6437f67 100644 --- a/project/services/importer/ld_json_importer.py +++ b/project/services/importer/ld_json_importer.py @@ -343,7 +343,6 @@ class LdJsonImporter: image_items = image if isinstance(image, list) else [image] for image_item in image_items: - if isinstance(image_item, str): image_item = {"url": image_item} diff --git a/project/views/event.py b/project/views/event.py index 25af426..8126a11 100644 --- a/project/views/event.py +++ b/project/views/event.py @@ -398,7 +398,6 @@ def send_referenced_event_changed_mails(event): # Alle Referenzen references = EventReference.query.filter(EventReference.event_id == event.id).all() for reference in references: - # Alle Mitglieder der AdminUnit, die das Recht haben, Requests zu verifizieren members = get_admin_unit_members_with_permission( reference.admin_unit_id, "reference_request:verify" diff --git a/requirements.txt b/requirements.txt index 4841bd6..f5ffbfc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,13 +10,13 @@ Authlib==0.15.3 Babel==2.9.1 bcrypt==3.2.0 beautifulsoup4==4.9.3 -black==20.8b1 +black==23.1.0 blinker==1.4 certifi==2020.12.5 cffi==1.14.4 cfgv==3.2.0 chardet==3.0.4 -click==7.1.2 +click==8.1.3 colour==0.1.5 coverage==5.5 coveralls==2.2.0 @@ -68,11 +68,12 @@ mistune==0.8.4 mypy-extensions==0.4.3 nodeenv==1.5.0 oauthlib==3.1.0 -packaging==20.8 +packaging==23.0 passlib==1.7.4 -pathspec==0.8.1 +pathspec==0.11.0 pilkit==2.0 Pillow==9.0.0 +platformdirs==3.1.0 pluggy==0.13.1 pre-commit==2.9.3 psycopg2-binary==2.8.6 @@ -106,8 +107,9 @@ SQLAlchemy-Utils==0.36.8 swagger-spec-validator==2.7.3 TatSu==4.4.0 toml==0.10.2 -typed-ast==1.4.1 -typing-extensions==3.7.4.3 +tomli==2.0.1 +typed-ast==1.5.4 +typing_extensions==4.5.0 urllib3==1.26.5 URLObject==2.4.3 validators==0.18.2 diff --git a/tests/services/importer/test_event_importer.py b/tests/services/importer/test_event_importer.py index c53a703..7800292 100644 --- a/tests/services/importer/test_event_importer.py +++ b/tests/services/importer/test_event_importer.py @@ -11,7 +11,6 @@ def test_import(client, seeder, utils, app, shared_datadir, requests_mock): params = (utils, admin_unit_id, shared_datadir) with app.app_context(): - _assert_import_event( params, "facebook.html", From c55183cfb79f2a6efca86f29cdec133e07366cfd Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Fri, 10 Mar 2023 08:20:36 +0100 Subject: [PATCH 3/5] Deployment with docker compose #376 --- project/__init__.py | 2 +- tests/views/test_root.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/project/__init__.py b/project/__init__.py index 1b2c79a..44135c1 100644 --- a/project/__init__.py +++ b/project/__init__.py @@ -15,7 +15,7 @@ from flask_wtf.csrf import CSRFProtect from project.custom_session_interface import CustomSessionInterface -def getenv_bool(name: str, default: str = "False"): +def getenv_bool(name: str, default: str = "False"): # pragma: no cover return os.getenv(name, default).lower() in ("true", "1", "t") diff --git a/tests/views/test_root.py b/tests/views/test_root.py index 778e786..cd3a72c 100644 --- a/tests/views/test_root.py +++ b/tests/views/test_root.py @@ -12,6 +12,10 @@ def test_home(client, seeder, utils): utils.assert_response_redirect(response, "home") +def test_up(app, utils): + utils.get_ok("up") + + def test_organizations(client, seeder, utils): url = utils.get_url("organizations") utils.get_ok(url) From 9efbf59e0da29ac830acce7c889d033c09706cca Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Fri, 10 Mar 2023 16:27:49 +0100 Subject: [PATCH 4/5] Deployment with docker compose #376 --- deployment/docker-compose/README.md | 6 +++ .../docker-compose/nginx.config.example | 42 +++++++++++++++++++ doc/deployment.md | 2 + 3 files changed, 50 insertions(+) create mode 100644 deployment/docker-compose/nginx.config.example diff --git a/deployment/docker-compose/README.md b/deployment/docker-compose/README.md index 9fcc683..251d021 100644 --- a/deployment/docker-compose/README.md +++ b/deployment/docker-compose/README.md @@ -21,3 +21,9 @@ docker compose up --force-recreate --detach ```sh ./update.sh ``` + +## Execute commands in web container + +```sh +docker compose exec -it web /bin/sh +``` diff --git a/deployment/docker-compose/nginx.config.example b/deployment/docker-compose/nginx.config.example new file mode 100644 index 0000000..fe99696 --- /dev/null +++ b/deployment/docker-compose/nginx.config.example @@ -0,0 +1,42 @@ +location ^~ /image/ { + root "/var/www/vhosts/oveda.de/cache/img"; + expires 1h; + + location ~ ^/image/(?[0-9]+)/(?[0-9]+) { + if ($arg_s = '') { + rewrite (.*) $1?s=500 last; + } + try_files /${id}-${hash}-${arg_s}-${arg_s}.png /${id}-${hash}-${arg_s}-${arg_s}.jpg @docker; + } + + location ~ ^/image/(?[0-9]+) { + if ($arg_s = '') { + rewrite (.*) $1?s=500 last; + } + try_files /${id}-${arg_s}-${arg_s}.png /${id}-${arg_s}-${arg_s}.jpg @docker; + } +} +location ^~ /static/ { + alias "/var/www/vhosts/oveda.de/static/"; + expires 1h; +} +location ^~ /dump/ { + alias "/var/www/vhosts/oveda.de/cache/dump/"; +} +location ^~ /sitemap.xml { + alias "/var/www/vhosts/oveda.de/cache/sitemap.xml"; +} +location ^~ /robots.txt { + alias "/var/www/vhosts/oveda.de/cache/robots.txt"; +} +location ^~ /favicon.ico { + alias "/var/www/vhosts/oveda.de/static/favicon.ico"; + expires 12h; +} +location @docker { + proxy_pass http://0.0.0.0:5000; + proxy_set_header Host $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; +} \ No newline at end of file diff --git a/doc/deployment.md b/doc/deployment.md index c93db6d..1ad297f 100644 --- a/doc/deployment.md +++ b/doc/deployment.md @@ -44,6 +44,8 @@ Jobs that should run on a regular basis. ```sh flask event update-recurring-dates flask dump all +flask seo generate-sitemap --pinggoogle +flask seo generate-robots-txt ``` ## Administration From 2028da2f7fe7a6dcfc127767372befd3caf6b748 Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Fri, 10 Mar 2023 16:46:03 +0100 Subject: [PATCH 5/5] Deployment with docker compose #376 --- deployment/docker-compose/README.md | 2 ++ deployment/docker-compose/update.sh | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/deployment/docker-compose/README.md b/deployment/docker-compose/README.md index 251d021..5e4797c 100644 --- a/deployment/docker-compose/README.md +++ b/deployment/docker-compose/README.md @@ -18,6 +18,8 @@ docker compose up --force-recreate --detach ## Update app +Adjust `WEB_TAG` in .env if necessary. + ```sh ./update.sh ``` diff --git a/deployment/docker-compose/update.sh b/deployment/docker-compose/update.sh index 32f7d80..09075b4 100755 --- a/deployment/docker-compose/update.sh +++ b/deployment/docker-compose/update.sh @@ -2,5 +2,5 @@ set -e docker compose pull web docker compose stop web -docker compose run db-backup /backup.sh +docker compose exec db-backup /backup.sh docker compose up --detach --force-recreate web \ No newline at end of file