Skip to content

Commit 895660b

Browse files
committed
WIP: shared filesystem for database storage (louking/webmodules#10)
1 parent ea289a1 commit 895660b

26 files changed

+1302
-0
lines changed

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,10 @@ cython_debug/
158158
# and can be added to the global gitignore or merged into this file. For a more nuclear
159159
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
160160
#.idea/
161+
162+
# local configuration files
163+
*.json
164+
*.cfg
165+
*-config.js
166+
*_config.py
167+
config/

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
# mysql-docker
22
mysql, phpmyadmin, crond backups in docker compose
3+
4+
provide docker container with services to support database

app/Dockerfile

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
ARG PYTHON_VER
2+
3+
# For more information, please refer to https://aka.ms/vscode-docker-python
4+
FROM python:${PYTHON_VER}-alpine
5+
6+
# EXPOSE 5000
7+
8+
# Keeps Python from generating .pyc files in the container
9+
ENV PYTHONDONTWRITEBYTECODE=1
10+
11+
# Turns off buffering for easier container logging
12+
ENV PYTHONUNBUFFERED=1
13+
14+
# set the working directory in the container
15+
WORKDIR /app
16+
17+
# Install pip requirements
18+
COPY requirements.txt .
19+
20+
# needed for mysqlclient package to compile
21+
# adapted from https://github.com/gliderlabs/docker-alpine/issues/181#issuecomment-444857401
22+
# and https://github.com/gliderlabs/docker-alpine/issues/181#issuecomment-348608168
23+
RUN apk add --no-cache mariadb-connector-c-dev \
24+
&& apk add --no-cache --virtual .build-deps \
25+
build-base \
26+
mariadb-dev \
27+
&& pip install -r requirements.txt \
28+
&& rm -rf .cache/pip \
29+
&& apk del .build-deps
30+
31+
# we'll be doing mysql, mysqldump commands
32+
RUN apk add --no-cache mysql-client
33+
34+
# we're going to want to use bash
35+
RUN apk add bash
36+
37+
# copy the content of the local src directory to the working directory
38+
# this isn't needed when developing as there's a bind under volumes: in the docker-compose.dev.yml file
39+
COPY src .
40+
41+
# Creates a non-root user with an explicit UID and adds permission to access the /app folder
42+
# For more info, please refer to https://aka.ms/vscode-docker-python-configure-containers
43+
RUN chmod +x /app/dbupgrade_and_run.sh && adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app
44+
USER appuser

app/requirements.txt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
alembic==1.11.1
2+
bcrypt==4.0.1
3+
blinker==1.6.2
4+
cffi==1.15.1
5+
click==8.1.3
6+
colorama==0.4.6
7+
cryptography==41.0.1
8+
decorator==5.1.1
9+
dnspython==2.3.0
10+
email-validator==2.0.0.post2
11+
fabric==3.1.0
12+
Flask==2.3.2
13+
Flask-Login==0.6.2
14+
Flask-Migrate==4.0.4
15+
Flask-Principal==0.4.0
16+
Flask-Security-Too==5.2.0
17+
Flask-SQLAlchemy==3.0.5
18+
Flask-WTF==1.1.1
19+
greenlet==2.0.2
20+
idna==3.4
21+
importlib-metadata==6.7.0
22+
importlib-resources==5.12.0
23+
invoke==2.1.3
24+
itsdangerous==2.1.2
25+
Jinja2==3.1.2
26+
loutilities==3.8.3
27+
Mako==1.2.4
28+
MarkupSafe==2.1.3
29+
mysqlclient==2.2.0
30+
paramiko==3.2.0
31+
passlib==1.7.4
32+
pycparser==2.21
33+
PyNaCl==1.5.0
34+
pytz==2023.3
35+
SQLAlchemy==2.0.17
36+
typing_extensions==4.6.3
37+
tzdata==2023.3
38+
tzlocal==5.0.1
39+
Werkzeug==2.3.6
40+
WTForms==3.0.1
41+
zipp==3.15.0
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/bin/bash
2+
3+
# NOTE: file_env, docker_process_sql, _mysql_passfile, logging functions COPIED from
4+
# https://github.com/docker-library/mysql/blob/master/8.0/docker-entrypoint.sh
5+
6+
# logging functions
7+
mysql_log() {
8+
local type="$1"; shift
9+
# accept argument string or stdin
10+
local text="$*"; if [ "$#" -eq 0 ]; then text="$(cat)"; fi
11+
local dt; dt="$(date -Iseconds)"
12+
printf '%s [%s] [Entrypoint]: %s\n' "$dt" "$type" "$text"
13+
}
14+
mysql_note() {
15+
mysql_log Note "$@"
16+
}
17+
mysql_warn() {
18+
mysql_log Warn "$@" >&2
19+
}
20+
mysql_error() {
21+
mysql_log ERROR "$@" >&2
22+
exit 1
23+
}
24+
25+
# usage: file_env VAR [DEFAULT]
26+
# ie: file_env 'XYZ_DB_PASSWORD' 'example'
27+
# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of
28+
# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature)
29+
file_env() {
30+
local var="$1"
31+
local fileVar="${var}_FILE"
32+
local def="${2:-}"
33+
if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
34+
mysql_error "Both $var and $fileVar are set (but are exclusive)"
35+
fi
36+
local val="$def"
37+
if [ "${!var:-}" ]; then
38+
val="${!var}"
39+
elif [ "${!fileVar:-}" ]; then
40+
val="$(< "${!fileVar}")"
41+
fi
42+
export "$var"="$val"
43+
unset "$fileVar"
44+
}
45+
46+
_mysql_passfile() {
47+
# echo the password to the "file" the client uses
48+
# the client command will use process substitution to create a file on the fly
49+
# ie: --defaults-extra-file=<( _mysql_passfile )
50+
cat <<-EOF
51+
[client]
52+
password="`cat /run/secrets/db-password`"
53+
EOF
54+
# note use of tab character above
55+
}
56+
57+
# Execute sql script, passed via stdin
58+
# ie: docker_process_sql <<<'INSERT ...'
59+
# ie: docker_process_sql <my-file.sql
60+
docker_process_sql() {
61+
# default mysql but caller can override
62+
mysql --defaults-extra-file=<( _mysql_passfile) -uroot -hdb --comments --database=mysql "$@"
63+
}
64+
65+
# follow pattern for each application database (https://stackoverflow.com/a/68714439/799921)
66+
file_env APP_PASSWORD
67+
echo "Creating database \`${APP_DATABASE}\`"
68+
mysql_note "Creating database \`${APP_DATABASE}\`"
69+
docker_process_sql <<<"CREATE DATABASE IF NOT EXISTS \`$APP_DATABASE\`;" || exit 1
70+
mysql_note "Creating user ${APP_USER}"
71+
docker_process_sql <<<"CREATE USER IF NOT EXISTS '$APP_USER'@'%' IDENTIFIED BY '$APP_PASSWORD' ;" || exit 1
72+
mysql_note "Giving user ${APP_USER} access to schema ${APP_DATABASE}"
73+
# grant access for user if not done before. see https://dba.stackexchange.com/a/105376
74+
docker_process_sql <<<"GRANT ALL ON \`${APP_DATABASE//_/\\_}\`.* TO '$APP_USER'@'%' ;" || exit 1
75+
# docker_process_sql <<<cat <<-EOF
76+
# SET @sql_found='SELECT 1 INTO @x';
77+
# SET @sql_fresh="GRANT ALL ON \`${APP_DATABASE//_/\\_}\`.* TO '$APP_USER'@'%' ;";
78+
# SELECT COUNT(1) INTO @found_count FROM mysql.user WHERE user='$APP_USER' AND host='%';
79+
# SET @sql=IF(@found_count=1,@sql_found,@sql_fresh);
80+
# PREPARE s FROM @sql;
81+
# EXECUTE s;
82+
# DEALLOCATE PREPARE s;
83+
# EOF
84+
if [ $? -ne 0 ]; then
85+
exit 1
86+
fi

app/src/app.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
'''
2+
app.py is only used to support flask commands
3+
4+
server execution from run.py
5+
'''
6+
7+
# standard
8+
import os.path
9+
10+
# pypi
11+
from flask_migrate import Migrate
12+
from mysql_docker.model import db
13+
14+
# homegrown
15+
from mysql_docker import create_app, appname
16+
from mysql_docker.settings import Production
17+
from scripts import InitCli
18+
19+
abspath = os.path.abspath('/config')
20+
configpath = os.path.join(abspath, f'{appname}.cfg')
21+
configfiles = [configpath]
22+
userconfigpath = os.path.join(abspath, 'users.cfg')
23+
# userconfigpath first so configpath can override
24+
configfiles.insert(0, userconfigpath)
25+
26+
# init_for_operation=False because when we create app this would use database and cause
27+
# sqlalchemy.exc.OperationalError if one of the updating tables needs migration
28+
app = create_app(Production(configfiles), configfiles, init_for_operation=False)
29+
30+
# set up flask command processing
31+
migrate_cli = Migrate(app, db, compare_type=True)
32+
init_cli = InitCli(app, db)
33+
34+
## Needed only if serving web pages
35+
# # implement proxy fix (https://github.com/sjmf/reverse-proxy-minimal-example)
36+
# from werkzeug.middleware.proxy_fix import ProxyFix
37+
# app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_host=1, x_port=1, x_proto=1, x_prefix=1)
38+
39+
# if __name__ == "__main__":
40+
# app.run(host ='0.0.0.0', port=5000)

app/src/dbupgrade_and_run.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/bin/sh
2+
3+
# NOTE: file end of line characters must be LF, not CRLF (see https://stackoverflow.com/a/58220487/799921)
4+
5+
# create database if necessary
6+
while ! ./app-initdb.d/create-database.sh
7+
do
8+
sleep 5
9+
done
10+
11+
# initial volume create may cause flask db upgrade to fail
12+
while ! flask db upgrade
13+
do
14+
sleep 5
15+
done
16+
exec "$@"

app/src/migrations/README

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Multi-database configuration for Flask.

app/src/migrations/alembic.ini

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# A generic, single database configuration.
2+
3+
[alembic]
4+
# template used to generate migration files
5+
# file_template = %%(rev)s_%%(slug)s
6+
7+
# set to 'true' to run the environment during
8+
# the 'revision' command, regardless of autogenerate
9+
# revision_environment = false
10+
11+
12+
# Logging configuration
13+
[loggers]
14+
keys = root,sqlalchemy,alembic,flask_migrate
15+
16+
[handlers]
17+
keys = console
18+
19+
[formatters]
20+
keys = generic
21+
22+
[logger_root]
23+
level = WARN
24+
handlers = console
25+
qualname =
26+
27+
[logger_sqlalchemy]
28+
level = WARN
29+
handlers =
30+
qualname = sqlalchemy.engine
31+
32+
[logger_alembic]
33+
level = INFO
34+
handlers =
35+
qualname = alembic
36+
37+
[logger_flask_migrate]
38+
level = INFO
39+
handlers =
40+
qualname = flask_migrate
41+
42+
[handler_console]
43+
class = StreamHandler
44+
args = (sys.stderr,)
45+
level = NOTSET
46+
formatter = generic
47+
48+
[formatter_generic]
49+
format = %(levelname)-5.5s [%(name)s] %(message)s
50+
datefmt = %H:%M:%S

0 commit comments

Comments
 (0)