Browse Source

backend: Add Docker support

ble^2
Andreas Berthoud 4 years ago
parent
commit
d681995de0
  1. 4
      backend/.dockerignore
  2. 1
      backend/.gitignore
  3. 25
      backend/docker-compose.yml
  4. 6
      backend/docker-entrypoint.sh
  5. 3
      backend/docker_build_and_upload.sh
  6. 45
      backend/dockerfile
  7. 12
      backend/monsun_backend/__init__.py
  8. 3
      backend/monsun_backend/command_execution.py
  9. 47
      backend/nginx.conf
  10. 3
      backend/pytest.ini
  11. 74
      backend/system_tests/test_login.py
  12. 12
      backend/tests/docker-compose.yml
  13. 2
      backend/tests/requirements.txt
  14. 12
      backend/uwsgi.ini
  15. 79
      backend/wait-for-it.sh

4
backend/.dockerignore

@ -0,0 +1,4 @@
**/__pycache__/
build/
dist/
**/*.egg-info

1
backend/.gitignore

@ -2,3 +2,4 @@
build/
dist/
**/*.egg-info
db-data/

25
backend/docker-compose.yml

@ -0,0 +1,25 @@
version: "2"
services:
monsun_postgres:
image: postgres
container_name: monsun_postgres
restart: always
volumes:
- ./db-data:/var/lib/postgresql/data
env_file: dev.env
monsun_backend:
build: .
container_name: monsun_backd
restart: always
ports:
- 80:80
volumes:
- ./config:/var/config/
depends_on:
- monsun_postgres
environment:
POSTGRES_HOST: monsun_postgres
env_file: dev.env
command: ["bash", "./wait-for-it.sh", "monsun_postgres:5432", "-t", "60", "--", "./docker-entrypoint.sh"]

6
backend/docker-entrypoint.sh

@ -0,0 +1,6 @@
#!/usr/bin/env bash
source /opt/venv/bin/activate
flask db upgrade
service nginx start
uwsgi --ini uwsgi.ini

3
backend/docker_build_and_upload.sh

@ -0,0 +1,3 @@
docker build -t monsun_backend .
docker tag monsun_backend:latest registry.berthoud.dev/monsun_backend
docker push registry.berthoud.dev/monsun_backend:latest

45
backend/dockerfile

@ -0,0 +1,45 @@
# stage 1
FROM python:3.8-slim-buster as backend-build
VOLUME /app
WORKDIR /app
RUN apt-get update \
&& apt-get -y install python3-dev \
&& apt-get -y install build-essential
RUN python3 -m venv /opt/venv
COPY . .
RUN . /opt/venv/bin/activate \
&& pip install --upgrade setuptools wheel \
&& pip install -r requirements.txt \
&& python setup.py sdist bdist_wheel \
&& pip install monsun_backend --no-index --find-links file:///app/dist
# stage 2
FROM python:3.8-slim-buster
RUN apt-get update \
&& apt-get -y install nginx \
&& apt-get -y install python3-dev \
&& apt-get -y install build-essential \
&& apt-get -qy install netcat
RUN mkdir /var/config
VOLUME /var/config
VOLUME /app
WORKDIR /app
COPY docker-entrypoint.sh /app/docker-entrypoint.sh
COPY wait-for-it.sh /app/wait-for-it.sh
COPY wsgi.py /app/wsgi.py
COPY nginx.conf /etc/nginx
COPY uwsgi.ini /app/uwsgi.ini
COPY migrations /app/migrations
COPY --from=backend-build /opt/venv /opt/venv
CMD ["bash", "./docker-entrypoint.sh"]

12
backend/monsun_backend/__init__.py

@ -1,13 +1,17 @@
import logging
import os
from logging.config import dictConfig
from flask_api import FlaskAPI
from flask_marshmallow import Marshmallow
"""https://blog.miguelgrinberg.com/post/how-to-add-flask-migrate-to-an-existing-project""" # noqa
import os
from flask_migrate import Migrate
from flask_security import Security
from sqlalchemy import create_engine
from sqlalchemy_utils import create_database
from sqlalchemy_utils import database_exists
from . import access
from . import command_execution
@ -48,6 +52,12 @@ def create_app() -> FlaskAPI:
app.register_blueprint(logout.bp)
app.register_blueprint(admin.bp)
# somehow the 'flask db upgrade' does not create the DB as promised...
# so let's do it here instead
engine = create_engine(os.getenv("DATABASE_URI"))
if not database_exists(engine.url):
create_database(engine.url)
dictConfig(
{
"version": 1,

3
backend/monsun_backend/command_execution.py

@ -17,8 +17,6 @@ from typing import Tuple
from serial import Serial
from backend.monsun_backend.util import log_function_call
from . import commands
from .commands import Command
from .commands import CommandId
@ -27,6 +25,7 @@ from .commands import Request
from .commands import Response
from .commands import get_response_class
from .container import get_initialize_container
from .util import log_function_call
_logger = logging.getLogger(__file__)

47
backend/nginx.conf

@ -0,0 +1,47 @@
user www-data;
worker_processes auto;
pid /run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
access_log /dev/stdout;
error_log /dev/stdout;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 20M;
include /etc/nginx/mime.types;
default_type application/octet-stream;
index index.html index.htm;
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name localhost;
root /var/www/html;
location / {
include uwsgi_params;
uwsgi_pass unix:/tmp/uwsgi.socket;
uwsgi_read_timeout 1h;
uwsgi_send_timeout 1h;
proxy_send_timeout 1h;
proxy_read_timeout 1h;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
}
}
}

3
backend/pytest.ini

@ -3,3 +3,6 @@ addopts = -s
markers =
client: required client device (dongle)
env =
D:TEST_BASE_URL=https://monsun.berthoud.dev

74
backend/system_tests/test_login.py

@ -0,0 +1,74 @@
import logging
import os
import pytest
import requests
from flask_api import status
BASE_URL = os.environ.get("TEST_BASE_URL", "http://localhost")
LOGIN_URI = BASE_URL + "/login"
LOGOUT_URI = BASE_URL + "/logout"
ADMIN_URI = BASE_URL + "/admin"
PASSWORD = os.environ.get("MONSUN_PASSWORD", "password")
@pytest.fixture(autouse=True)
def log_variables():
logger = logging.getLogger("system tests")
logger.info(f"BASE_URL: {BASE_URL}")
@pytest.fixture()
def session() -> requests.Session:
session = requests.Session()
response_login = session.post(
url=LOGIN_URI,
data={"email": "andreasberthoud@gmail.com", "password": PASSWORD},
)
assert response_login.status_code == status.HTTP_200_OK
return session
def test_login_as_admin_and_accessing_admin_endpoint():
session_admin_ = requests.Session()
response_login = session_admin_.post(
url=LOGIN_URI,
data={"email": "andreasberthoud@gmail.com", "password": PASSWORD},
)
assert response_login.status_code == status.HTTP_200_OK
response_admin_logged_in = session_admin_.get(
url=ADMIN_URI,
)
assert response_admin_logged_in.status_code == status.HTTP_200_OK
response_logout = session_admin_.delete(
url=LOGOUT_URI,
)
assert response_logout.status_code == status.HTTP_200_OK
response_admin_logged_out = session_admin_.get(
url=ADMIN_URI,
)
assert response_admin_logged_out.status_code == status.HTTP_403_FORBIDDEN
def test_toggle_client_led_then_status_is_ok(session):
response = session.post(
url=BASE_URL + "/client/command?cmd=led&id=red&command=toggle",
)
assert response.status_code == status.HTTP_200_OK
def test_pair_with_server_then_status_is_ok(session):
response = session.post(url=BASE_URL + "/client/command?cmd=gp&command_id=1")
assert response.status_code == status.HTTP_200_OK
def test_toggle_server_led_then_status_is_ok(session):
"""Requires a connected GATT server"""
response = session.post(
url=BASE_URL + "/client/command?cmd=led&id=red&command=toggle&target=server",
)
assert response.status_code == status.HTTP_200_OK

12
backend/tests/docker-compose.yml

@ -0,0 +1,12 @@
version: "2"
services:
tests_monsun_postgres-test:
image: postgres
environment:
POSTGRES_PASSWORD: pass
POSTGRES_USER: usr
POSTGRES_DB: sqlalchemy
POSTGRES_HOST: postgres
ports:
- 5432:5432

2
backend/tests/requirements.txt

@ -1 +1,3 @@
pytest
pytest-env
requests

12
backend/uwsgi.ini

@ -0,0 +1,12 @@
[uwsgi]
wsgi-file = wsgi.py
uid = www-data
gid = www-data
master = true
processes = 5
socket = /tmp/uwsgi.socket
chmod-sock = 664
vacuum = true
die-on-term = true

79
backend/wait-for-it.sh

@ -0,0 +1,79 @@
#!/bin/sh
# https://github.com/vishnubob/wait-for-it
TIMEOUT=15
QUIET=0
echoerr() {
if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi
}
usage() {
exitcode="$1"
cat << USAGE >&2
Usage:
$cmdname host:port [-t timeout] [-- command args]
-q | --quiet Do not output any status messages
-t TIMEOUT | --timeout=timeout Timeout in seconds, zero for no timeout
-- COMMAND ARGS Execute command with args after the test finishes
USAGE
exit "$exitcode"
}
wait_for() {
for i in `seq $TIMEOUT` ; do
nc -z "$HOST" "$PORT" > /dev/null 2>&1
result=$?
if [ $result -eq 0 ] ; then
if [ $# -gt 0 ] ; then
exec "$@"
fi
exit 0
fi
sleep 1
done
echo "Operation timed out" >&2
exit 1
}
while [ $# -gt 0 ]
do
case "$1" in
*:* )
HOST=$(printf "%s\n" "$1"| cut -d : -f 1)
PORT=$(printf "%s\n" "$1"| cut -d : -f 2)
shift 1
;;
-q | --quiet)
QUIET=1
shift 1
;;
-t)
TIMEOUT="$2"
if [ "$TIMEOUT" = "" ]; then break; fi
shift 2
;;
--timeout=*)
TIMEOUT="${1#*=}"
shift 1
;;
--)
shift
break
;;
--help)
usage 0
;;
*)
echoerr "Unknown argument: $1"
usage 1
;;
esac
done
if [ "$HOST" = "" -o "$PORT" = "" ]; then
echoerr "Error: you need to provide a host and port to test."
usage 2
fi
wait_for "$@"
Loading…
Cancel
Save