From dcfec88e0b838adecec698812b5e6288a91a0967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 7 Mar 2019 15:15:28 +0400 Subject: [PATCH 1/5] :sparkles: Add autoreload for development --- python3.6-alpine3.8/Dockerfile | 3 ++ python3.6-alpine3.8/start-reload.sh | 28 ++++++++++ python3.6/Dockerfile | 3 ++ python3.6/start-reload.sh | 28 ++++++++++ python3.7-alpine3.8/Dockerfile | 3 ++ python3.7-alpine3.8/start-reload.sh | 28 ++++++++++ python3.7/Dockerfile | 3 ++ python3.7/start-reload.sh | 28 ++++++++++ tests/test_03_reload/__init__.py | 0 tests/test_03_reload/test_defaults.py | 70 +++++++++++++++++++++++++ tests/test_03_reload/test_env_vars_1.py | 70 +++++++++++++++++++++++++ tests/test_03_reload/test_env_vars_2.py | 50 ++++++++++++++++++ 12 files changed, 314 insertions(+) create mode 100644 python3.6-alpine3.8/start-reload.sh create mode 100644 python3.6/start-reload.sh create mode 100644 python3.7-alpine3.8/start-reload.sh create mode 100644 python3.7/start-reload.sh create mode 100644 tests/test_03_reload/__init__.py create mode 100644 tests/test_03_reload/test_defaults.py create mode 100644 tests/test_03_reload/test_env_vars_1.py create mode 100644 tests/test_03_reload/test_env_vars_2.py diff --git a/python3.6-alpine3.8/Dockerfile b/python3.6-alpine3.8/Dockerfile index 44a53022..11799c82 100644 --- a/python3.6-alpine3.8/Dockerfile +++ b/python3.6-alpine3.8/Dockerfile @@ -11,6 +11,9 @@ RUN chmod +x /start.sh COPY ./gunicorn_conf.py /gunicorn_conf.py +COPY ./start-reload.sh /start-reload.sh +RUN chmod +x /start-reload.sh + COPY ./app /app WORKDIR /app/ diff --git a/python3.6-alpine3.8/start-reload.sh b/python3.6-alpine3.8/start-reload.sh new file mode 100644 index 00000000..01a9e203 --- /dev/null +++ b/python3.6-alpine3.8/start-reload.sh @@ -0,0 +1,28 @@ +#! /usr/bin/env sh +set -e + +if [ -f /app/app/main.py ]; then + DEFAULT_MODULE_NAME=app.main +elif [ -f /app/main.py ]; then + DEFAULT_MODULE_NAME=main +fi +MODULE_NAME=${MODULE_NAME:-$DEFAULT_MODULE_NAME} +VARIABLE_NAME=${VARIABLE_NAME:-app} +export APP_MODULE=${APP_MODULE:-"$MODULE_NAME:$VARIABLE_NAME"} + +HOST=${HOST:-0.0.0.0} +PORT=${PORT:-80} +LOG_LEVEL=${LOG_LEVEL:-info} + +# If there's a prestart.sh script in the /app directory, run it before starting +PRE_START_PATH=/app/prestart.sh +echo "Checking for script in $PRE_START_PATH" +if [ -f $PRE_START_PATH ] ; then + echo "Running script $PRE_START_PATH" + . "$PRE_START_PATH" +else + echo "There is no script $PRE_START_PATH" +fi + +# Start Uvicorn with live reload +exec uvicorn --reload --host $HOST --port $PORT --log-level $LOG_LEVEL "$APP_MODULE" diff --git a/python3.6/Dockerfile b/python3.6/Dockerfile index 7436b0de..b6646838 100644 --- a/python3.6/Dockerfile +++ b/python3.6/Dockerfile @@ -9,6 +9,9 @@ RUN chmod +x /start.sh COPY ./gunicorn_conf.py /gunicorn_conf.py +COPY ./start-reload.sh /start-reload.sh +RUN chmod +x /start-reload.sh + COPY ./app /app WORKDIR /app/ diff --git a/python3.6/start-reload.sh b/python3.6/start-reload.sh new file mode 100644 index 00000000..01a9e203 --- /dev/null +++ b/python3.6/start-reload.sh @@ -0,0 +1,28 @@ +#! /usr/bin/env sh +set -e + +if [ -f /app/app/main.py ]; then + DEFAULT_MODULE_NAME=app.main +elif [ -f /app/main.py ]; then + DEFAULT_MODULE_NAME=main +fi +MODULE_NAME=${MODULE_NAME:-$DEFAULT_MODULE_NAME} +VARIABLE_NAME=${VARIABLE_NAME:-app} +export APP_MODULE=${APP_MODULE:-"$MODULE_NAME:$VARIABLE_NAME"} + +HOST=${HOST:-0.0.0.0} +PORT=${PORT:-80} +LOG_LEVEL=${LOG_LEVEL:-info} + +# If there's a prestart.sh script in the /app directory, run it before starting +PRE_START_PATH=/app/prestart.sh +echo "Checking for script in $PRE_START_PATH" +if [ -f $PRE_START_PATH ] ; then + echo "Running script $PRE_START_PATH" + . "$PRE_START_PATH" +else + echo "There is no script $PRE_START_PATH" +fi + +# Start Uvicorn with live reload +exec uvicorn --reload --host $HOST --port $PORT --log-level $LOG_LEVEL "$APP_MODULE" diff --git a/python3.7-alpine3.8/Dockerfile b/python3.7-alpine3.8/Dockerfile index 21c2fa90..e828aab8 100644 --- a/python3.7-alpine3.8/Dockerfile +++ b/python3.7-alpine3.8/Dockerfile @@ -11,6 +11,9 @@ RUN chmod +x /start.sh COPY ./gunicorn_conf.py /gunicorn_conf.py +COPY ./start-reload.sh /start-reload.sh +RUN chmod +x /start-reload.sh + COPY ./app /app WORKDIR /app/ diff --git a/python3.7-alpine3.8/start-reload.sh b/python3.7-alpine3.8/start-reload.sh new file mode 100644 index 00000000..01a9e203 --- /dev/null +++ b/python3.7-alpine3.8/start-reload.sh @@ -0,0 +1,28 @@ +#! /usr/bin/env sh +set -e + +if [ -f /app/app/main.py ]; then + DEFAULT_MODULE_NAME=app.main +elif [ -f /app/main.py ]; then + DEFAULT_MODULE_NAME=main +fi +MODULE_NAME=${MODULE_NAME:-$DEFAULT_MODULE_NAME} +VARIABLE_NAME=${VARIABLE_NAME:-app} +export APP_MODULE=${APP_MODULE:-"$MODULE_NAME:$VARIABLE_NAME"} + +HOST=${HOST:-0.0.0.0} +PORT=${PORT:-80} +LOG_LEVEL=${LOG_LEVEL:-info} + +# If there's a prestart.sh script in the /app directory, run it before starting +PRE_START_PATH=/app/prestart.sh +echo "Checking for script in $PRE_START_PATH" +if [ -f $PRE_START_PATH ] ; then + echo "Running script $PRE_START_PATH" + . "$PRE_START_PATH" +else + echo "There is no script $PRE_START_PATH" +fi + +# Start Uvicorn with live reload +exec uvicorn --reload --host $HOST --port $PORT --log-level $LOG_LEVEL "$APP_MODULE" diff --git a/python3.7/Dockerfile b/python3.7/Dockerfile index 5f26a0b1..e42e140a 100644 --- a/python3.7/Dockerfile +++ b/python3.7/Dockerfile @@ -9,6 +9,9 @@ RUN chmod +x /start.sh COPY ./gunicorn_conf.py /gunicorn_conf.py +COPY ./start-reload.sh /start-reload.sh +RUN chmod +x /start-reload.sh + COPY ./app /app WORKDIR /app/ diff --git a/python3.7/start-reload.sh b/python3.7/start-reload.sh new file mode 100644 index 00000000..01a9e203 --- /dev/null +++ b/python3.7/start-reload.sh @@ -0,0 +1,28 @@ +#! /usr/bin/env sh +set -e + +if [ -f /app/app/main.py ]; then + DEFAULT_MODULE_NAME=app.main +elif [ -f /app/main.py ]; then + DEFAULT_MODULE_NAME=main +fi +MODULE_NAME=${MODULE_NAME:-$DEFAULT_MODULE_NAME} +VARIABLE_NAME=${VARIABLE_NAME:-app} +export APP_MODULE=${APP_MODULE:-"$MODULE_NAME:$VARIABLE_NAME"} + +HOST=${HOST:-0.0.0.0} +PORT=${PORT:-80} +LOG_LEVEL=${LOG_LEVEL:-info} + +# If there's a prestart.sh script in the /app directory, run it before starting +PRE_START_PATH=/app/prestart.sh +echo "Checking for script in $PRE_START_PATH" +if [ -f $PRE_START_PATH ] ; then + echo "Running script $PRE_START_PATH" + . "$PRE_START_PATH" +else + echo "There is no script $PRE_START_PATH" +fi + +# Start Uvicorn with live reload +exec uvicorn --reload --host $HOST --port $PORT --log-level $LOG_LEVEL "$APP_MODULE" diff --git a/tests/test_03_reload/__init__.py b/tests/test_03_reload/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_03_reload/test_defaults.py b/tests/test_03_reload/test_defaults.py new file mode 100644 index 00000000..087dace1 --- /dev/null +++ b/tests/test_03_reload/test_defaults.py @@ -0,0 +1,70 @@ +import time + +import docker +import pytest +import requests +from docker.models.containers import Container + +from ..utils import CONTAINER_NAME, get_logs, remove_previous_container + +client = docker.from_env() + + +def verify_container(container, response_text): + logs = get_logs(container) + assert "Checking for script in /app/prestart.sh" in logs + assert "Running script /app/prestart.sh" in logs + assert ( + "Running inside /app/prestart.sh, you could add migrations to this file" in logs + ) + assert "Uvicorn running on http://0.0.0.0:80" in logs + response = requests.get("http://127.0.0.1:8000") + assert response.text == response_text + + +@pytest.mark.parametrize( + "image,response_text", + [ + ( + "tiangolo/uvicorn-gunicorn:python3.6", + "Hello world! From Uvicorn with Gunicorn. Using Python 3.6", + ), + ( + "tiangolo/uvicorn-gunicorn:python3.7", + "Hello world! From Uvicorn with Gunicorn. Using Python 3.7", + ), + ( + "tiangolo/uvicorn-gunicorn:latest", + "Hello world! From Uvicorn with Gunicorn. Using Python 3.7", + ), + ( + "tiangolo/uvicorn-gunicorn:python3.6-alpine3.8", + "Hello world! From Uvicorn with Gunicorn in Alpine. Using Python 3.6", + ), + ( + "tiangolo/uvicorn-gunicorn:python3.7-alpine3.8", + "Hello world! From Uvicorn with Gunicorn in Alpine. Using Python 3.7", + ), + ], +) +def test_defaults(image: str, response_text: str): + remove_previous_container(client) + container: Container = client.containers.run( + image, + name=CONTAINER_NAME, + ports={"80": "8000"}, + detach=True, + command="/start-reload.sh", + ) + time.sleep(1) + verify_container(container, response_text) + container.exec_run( + "sed -i 's|Uvicorn with Gunicorn|Uvicorn with autoreload|' /app/main.py" + ) + new_response_text = response_text.replace( + "Uvicorn with Gunicorn", "Uvicorn with autoreload" + ) + time.sleep(1) + verify_container(container, new_response_text) + container.stop() + container.remove() diff --git a/tests/test_03_reload/test_env_vars_1.py b/tests/test_03_reload/test_env_vars_1.py new file mode 100644 index 00000000..1e178552 --- /dev/null +++ b/tests/test_03_reload/test_env_vars_1.py @@ -0,0 +1,70 @@ +import time + +import docker +import pytest +import requests + +from ..utils import CONTAINER_NAME, remove_previous_container, get_logs + +client = docker.from_env() + + +def verify_container(container, response_text): + logs = get_logs(container) + assert "Checking for script in /app/prestart.sh" in logs + assert "Running script /app/prestart.sh" in logs + assert ( + "Running inside /app/prestart.sh, you could add migrations to this file" in logs + ) + assert "Uvicorn running on http://0.0.0.0:80" in logs + response = requests.get("http://127.0.0.1:8000") + assert response.text == response_text + + +@pytest.mark.parametrize( + "image,response_text", + [ + ( + "tiangolo/uvicorn-gunicorn:python3.6", + "Hello world! From Uvicorn with Gunicorn. Using Python 3.6", + ), + ( + "tiangolo/uvicorn-gunicorn:python3.7", + "Hello world! From Uvicorn with Gunicorn. Using Python 3.7", + ), + ( + "tiangolo/uvicorn-gunicorn:latest", + "Hello world! From Uvicorn with Gunicorn. Using Python 3.7", + ), + ( + "tiangolo/uvicorn-gunicorn:python3.6-alpine3.8", + "Hello world! From Uvicorn with Gunicorn in Alpine. Using Python 3.6", + ), + ( + "tiangolo/uvicorn-gunicorn:python3.7-alpine3.8", + "Hello world! From Uvicorn with Gunicorn in Alpine. Using Python 3.7", + ), + ], +) +def test_env_vars_1(image, response_text): + remove_previous_container(client) + container = client.containers.run( + image, + name=CONTAINER_NAME, + environment={"PORT": "8000", "LOG_LEVEL": "warning"}, + ports={"8000": "8000"}, + detach=True, + command="/start-reload.sh", + ) + time.sleep(1) + verify_container(container, response_text) + container.exec_run( + "sed -i 's|Uvicorn with Gunicorn|Uvicorn with autoreload|' /app/main.py" + ) + new_response_text = response_text.replace( + "Uvicorn with Gunicorn", "Uvicorn with autoreload" + ) + time.sleep(1) + verify_container(container, new_response_text) + container.stop() + container.remove() diff --git a/tests/test_03_reload/test_env_vars_2.py b/tests/test_03_reload/test_env_vars_2.py new file mode 100644 index 00000000..0b23324b --- /dev/null +++ b/tests/test_03_reload/test_env_vars_2.py @@ -0,0 +1,50 @@ +import time + +import docker +import pytest + +from ..utils import ( + CONTAINER_NAME, + get_config, + get_logs, + get_process_names, + remove_previous_container, +) + +client = docker.from_env() + + +def verify_container(container): + logs = get_logs(container) + assert "Checking for script in /app/prestart.sh" in logs + assert "Running script /app/prestart.sh" in logs + assert ( + "Running inside /app/prestart.sh, you could add migrations to this file" in logs + ) + assert "Uvicorn running on http://127.0.0.1:80" in logs + + +@pytest.mark.parametrize( + "image", + [ + ("tiangolo/uvicorn-gunicorn:python3.6"), + ("tiangolo/uvicorn-gunicorn:python3.7"), + ("tiangolo/uvicorn-gunicorn:latest"), + ("tiangolo/uvicorn-gunicorn:python3.6-alpine3.8"), + ("tiangolo/uvicorn-gunicorn:python3.7-alpine3.8"), + ], +) +def test_env_vars_2(image): + remove_previous_container(client) + container = client.containers.run( + image, + name=CONTAINER_NAME, + environment={"HOST": "127.0.0.1"}, + ports={"80": "8000"}, + detach=True, + command="/start-reload.sh", + ) + time.sleep(1) + verify_container(container) + container.stop() + container.remove() From 9a0588adb42c58ea4874cd2bb2897f297a25f6b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 7 Mar 2019 15:56:37 +0400 Subject: [PATCH 2/5] :bug: Fix tests for live reload env vars 1 --- tests/test_03_reload/test_env_vars_1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_03_reload/test_env_vars_1.py b/tests/test_03_reload/test_env_vars_1.py index 1e178552..e77c789a 100644 --- a/tests/test_03_reload/test_env_vars_1.py +++ b/tests/test_03_reload/test_env_vars_1.py @@ -51,7 +51,7 @@ def test_env_vars_1(image, response_text): container = client.containers.run( image, name=CONTAINER_NAME, - environment={"PORT": "8000", "LOG_LEVEL": "warning"}, + environment={"PORT": "8000", "LOG_LEVEL": "debug"}, ports={"8000": "8000"}, detach=True, command="/start-reload.sh", From 0e60fc7ac8bb271f89adc1f1d6bc4fd323474635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 7 Mar 2019 15:56:58 +0400 Subject: [PATCH 3/5] :memo: Update README --- README.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/README.md b/README.md index e46df63c..9c4ca036 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,14 @@ Then you can build your image from the directory that has your `Dockerfile`, e.g docker build -t myimage ./ ``` +* Run a container based on your image: + +```bash +docker run -d --name mycontainer -p 80:80 myimage +``` + +You should be able to check it in your Docker container's URL, for example: http://192.168.99.100/ or http://127.0.0.1/ (or equivalent, using your Docker host). + ## Advanced usage ### Environment variables @@ -333,6 +341,57 @@ If you need to run a Python script before starting the app, you could make the ` python /app/my_custom_prestart_script.py ``` +### Development live reload + +The default program that is run is at `/start.sh`. It does everything described above. + +There's also a version for development with live auto-reload at: + +```bash +/start-reload.sh +``` + +#### Details + +For development, it's useful to be able to mount the contents of the application code inside of the container as a Docker "host volume", to be able to change the code and test it live, without having to build the image every time. + +In that case, it's also useful to run the server with live auto-reload, so that it re-starts automatically at every code change. + +The additional script `/start-reload.sh` runs Uvicorn alone (without Gunicorn) and in a single process. + +It is ideal for development. + +#### Usage + +For example, instead of running: + +```bash +docker run -d -p 80:80 myimage +``` + +You could run: + +```bash +docker run -d -p 80:80 -v $(pwd):/app myimage /start-reload.sh +``` + +* `-v $(pwd):/app`: means that the directory `$(pwd)` should be mounted as a volume inside of the container at `/app`. + * `$(pwd)`: runs `pwd` ("print working directory") and puts it as part of the string. +* `/start-reload.sh`: adding something (like `/start-reload.sh`) at the end of the command, replaces the default "command" with this one. In this case, it replaces the default (`/start.sh`) with the development alternative `/start-reload.sh`. + +#### Technical Details + +As `/start-reload.sh` doesn't run with Gunicorn, any of the configurations you put in a `gunicorn_conf.py` file won't apply. + +But these environment variables will work the same as described above: + +* `MODULE_NAME` +* `VARIABLE_NAME` +* `APP_MODULE` +* `HOST` +* `PORT` +* `LOG_LEVEL` + ## Tests @@ -341,6 +400,10 @@ All the image tags, configurations, environment variables and application option ## Release Notes +### 0.4.0 + +* Add support for live auto-reload with an additional custom script `/start-reload.sh`. PR #6. + ### 0.3.0 * Set `WORKERS_PER_CORE` by default to `1`, as it shows to have the best performance on benchmarks. From c73af3f1822bf29da04f0ff405df81caf4625c46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 7 Mar 2019 16:16:55 +0400 Subject: [PATCH 4/5] :white_check_mark: Add tests for live reload app discovery --- tests/test_04_app_reload/__init__.py | 0 .../custom_app/app/custom_app/__init__.py | 0 .../custom_app/app/custom_app/custom_main.py | 24 ++++ .../custom_app/latest.dockerfile | 3 + .../custom_app/python3.6-alpine3.8.dockerfile | 3 + .../custom_app/python3.6.dockerfile | 3 + .../custom_app/python3.7-alpine3.8.dockerfile | 3 + .../custom_app/python3.7.dockerfile | 3 + .../package_app/app/app/__init__.py | 0 .../package_app/app/app/main.py | 24 ++++ .../package_app/latest.dockerfile | 3 + .../python3.6-alpine3.8.dockerfile | 3 + .../package_app/python3.6.dockerfile | 3 + .../python3.7-alpine3.8.dockerfile | 3 + .../package_app/python3.7.dockerfile | 3 + .../test_04_app_reload/simple_app/app/main.py | 24 ++++ .../simple_app/latest.dockerfile | 2 + .../simple_app/python3.6-alpine3.8.dockerfile | 3 + .../simple_app/python3.6.dockerfile | 3 + .../simple_app/python3.7-alpine3.8.dockerfile | 3 + .../simple_app/python3.7.dockerfile | 3 + tests/test_04_app_reload/test_custom_app.py | 110 ++++++++++++++++++ tests/test_04_app_reload/test_package_app.py | 75 ++++++++++++ tests/test_04_app_reload/test_simple_app.py | 73 ++++++++++++ 24 files changed, 374 insertions(+) create mode 100644 tests/test_04_app_reload/__init__.py create mode 100644 tests/test_04_app_reload/custom_app/app/custom_app/__init__.py create mode 100644 tests/test_04_app_reload/custom_app/app/custom_app/custom_main.py create mode 100644 tests/test_04_app_reload/custom_app/latest.dockerfile create mode 100644 tests/test_04_app_reload/custom_app/python3.6-alpine3.8.dockerfile create mode 100644 tests/test_04_app_reload/custom_app/python3.6.dockerfile create mode 100644 tests/test_04_app_reload/custom_app/python3.7-alpine3.8.dockerfile create mode 100644 tests/test_04_app_reload/custom_app/python3.7.dockerfile create mode 100644 tests/test_04_app_reload/package_app/app/app/__init__.py create mode 100644 tests/test_04_app_reload/package_app/app/app/main.py create mode 100644 tests/test_04_app_reload/package_app/latest.dockerfile create mode 100644 tests/test_04_app_reload/package_app/python3.6-alpine3.8.dockerfile create mode 100644 tests/test_04_app_reload/package_app/python3.6.dockerfile create mode 100644 tests/test_04_app_reload/package_app/python3.7-alpine3.8.dockerfile create mode 100644 tests/test_04_app_reload/package_app/python3.7.dockerfile create mode 100644 tests/test_04_app_reload/simple_app/app/main.py create mode 100644 tests/test_04_app_reload/simple_app/latest.dockerfile create mode 100644 tests/test_04_app_reload/simple_app/python3.6-alpine3.8.dockerfile create mode 100644 tests/test_04_app_reload/simple_app/python3.6.dockerfile create mode 100644 tests/test_04_app_reload/simple_app/python3.7-alpine3.8.dockerfile create mode 100644 tests/test_04_app_reload/simple_app/python3.7.dockerfile create mode 100644 tests/test_04_app_reload/test_custom_app.py create mode 100644 tests/test_04_app_reload/test_package_app.py create mode 100644 tests/test_04_app_reload/test_simple_app.py diff --git a/tests/test_04_app_reload/__init__.py b/tests/test_04_app_reload/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_04_app_reload/custom_app/app/custom_app/__init__.py b/tests/test_04_app_reload/custom_app/app/custom_app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_04_app_reload/custom_app/app/custom_app/custom_main.py b/tests/test_04_app_reload/custom_app/app/custom_app/custom_main.py new file mode 100644 index 00000000..f3fe1192 --- /dev/null +++ b/tests/test_04_app_reload/custom_app/app/custom_app/custom_main.py @@ -0,0 +1,24 @@ +import sys + + +class App: + def __init__(self, scope): + assert scope["type"] == "http" + self.scope = scope + + async def __call__(self, receive, send): + await send( + { + "type": "http.response.start", + "status": 200, + "headers": [[b"content-type", b"text/plain"]], + } + ) + version = f"{sys.version_info.major}.{sys.version_info.minor}" + message = f"Test app. From Uvicorn with Gunicorn. Using Python {version}".encode( + "utf-8" + ) + await send({"type": "http.response.body", "body": message}) + + +custom_var = App diff --git a/tests/test_04_app_reload/custom_app/latest.dockerfile b/tests/test_04_app_reload/custom_app/latest.dockerfile new file mode 100644 index 00000000..e1d7ff14 --- /dev/null +++ b/tests/test_04_app_reload/custom_app/latest.dockerfile @@ -0,0 +1,3 @@ +FROM tiangolo/uvicorn-gunicorn:latest + +COPY ./app /app diff --git a/tests/test_04_app_reload/custom_app/python3.6-alpine3.8.dockerfile b/tests/test_04_app_reload/custom_app/python3.6-alpine3.8.dockerfile new file mode 100644 index 00000000..4ca240f4 --- /dev/null +++ b/tests/test_04_app_reload/custom_app/python3.6-alpine3.8.dockerfile @@ -0,0 +1,3 @@ +FROM tiangolo/uvicorn-gunicorn:python3.6-alpine3.8 + +COPY ./app /app diff --git a/tests/test_04_app_reload/custom_app/python3.6.dockerfile b/tests/test_04_app_reload/custom_app/python3.6.dockerfile new file mode 100644 index 00000000..5373fb6b --- /dev/null +++ b/tests/test_04_app_reload/custom_app/python3.6.dockerfile @@ -0,0 +1,3 @@ +FROM tiangolo/uvicorn-gunicorn:python3.6 + +COPY ./app /app diff --git a/tests/test_04_app_reload/custom_app/python3.7-alpine3.8.dockerfile b/tests/test_04_app_reload/custom_app/python3.7-alpine3.8.dockerfile new file mode 100644 index 00000000..4c4e7769 --- /dev/null +++ b/tests/test_04_app_reload/custom_app/python3.7-alpine3.8.dockerfile @@ -0,0 +1,3 @@ +FROM tiangolo/uvicorn-gunicorn:python3.7-alpine3.8 + +COPY ./app /app diff --git a/tests/test_04_app_reload/custom_app/python3.7.dockerfile b/tests/test_04_app_reload/custom_app/python3.7.dockerfile new file mode 100644 index 00000000..b9be60e5 --- /dev/null +++ b/tests/test_04_app_reload/custom_app/python3.7.dockerfile @@ -0,0 +1,3 @@ +FROM tiangolo/uvicorn-gunicorn:python3.7 + +COPY ./app /app diff --git a/tests/test_04_app_reload/package_app/app/app/__init__.py b/tests/test_04_app_reload/package_app/app/app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_04_app_reload/package_app/app/app/main.py b/tests/test_04_app_reload/package_app/app/app/main.py new file mode 100644 index 00000000..03eadd01 --- /dev/null +++ b/tests/test_04_app_reload/package_app/app/app/main.py @@ -0,0 +1,24 @@ +import sys + + +class App: + def __init__(self, scope): + assert scope["type"] == "http" + self.scope = scope + + async def __call__(self, receive, send): + await send( + { + "type": "http.response.start", + "status": 200, + "headers": [[b"content-type", b"text/plain"]], + } + ) + version = f"{sys.version_info.major}.{sys.version_info.minor}" + message = f"Test app. From Uvicorn with Gunicorn. Using Python {version}".encode( + "utf-8" + ) + await send({"type": "http.response.body", "body": message}) + + +app = App diff --git a/tests/test_04_app_reload/package_app/latest.dockerfile b/tests/test_04_app_reload/package_app/latest.dockerfile new file mode 100644 index 00000000..e1d7ff14 --- /dev/null +++ b/tests/test_04_app_reload/package_app/latest.dockerfile @@ -0,0 +1,3 @@ +FROM tiangolo/uvicorn-gunicorn:latest + +COPY ./app /app diff --git a/tests/test_04_app_reload/package_app/python3.6-alpine3.8.dockerfile b/tests/test_04_app_reload/package_app/python3.6-alpine3.8.dockerfile new file mode 100644 index 00000000..4ca240f4 --- /dev/null +++ b/tests/test_04_app_reload/package_app/python3.6-alpine3.8.dockerfile @@ -0,0 +1,3 @@ +FROM tiangolo/uvicorn-gunicorn:python3.6-alpine3.8 + +COPY ./app /app diff --git a/tests/test_04_app_reload/package_app/python3.6.dockerfile b/tests/test_04_app_reload/package_app/python3.6.dockerfile new file mode 100644 index 00000000..5373fb6b --- /dev/null +++ b/tests/test_04_app_reload/package_app/python3.6.dockerfile @@ -0,0 +1,3 @@ +FROM tiangolo/uvicorn-gunicorn:python3.6 + +COPY ./app /app diff --git a/tests/test_04_app_reload/package_app/python3.7-alpine3.8.dockerfile b/tests/test_04_app_reload/package_app/python3.7-alpine3.8.dockerfile new file mode 100644 index 00000000..4c4e7769 --- /dev/null +++ b/tests/test_04_app_reload/package_app/python3.7-alpine3.8.dockerfile @@ -0,0 +1,3 @@ +FROM tiangolo/uvicorn-gunicorn:python3.7-alpine3.8 + +COPY ./app /app diff --git a/tests/test_04_app_reload/package_app/python3.7.dockerfile b/tests/test_04_app_reload/package_app/python3.7.dockerfile new file mode 100644 index 00000000..b9be60e5 --- /dev/null +++ b/tests/test_04_app_reload/package_app/python3.7.dockerfile @@ -0,0 +1,3 @@ +FROM tiangolo/uvicorn-gunicorn:python3.7 + +COPY ./app /app diff --git a/tests/test_04_app_reload/simple_app/app/main.py b/tests/test_04_app_reload/simple_app/app/main.py new file mode 100644 index 00000000..03eadd01 --- /dev/null +++ b/tests/test_04_app_reload/simple_app/app/main.py @@ -0,0 +1,24 @@ +import sys + + +class App: + def __init__(self, scope): + assert scope["type"] == "http" + self.scope = scope + + async def __call__(self, receive, send): + await send( + { + "type": "http.response.start", + "status": 200, + "headers": [[b"content-type", b"text/plain"]], + } + ) + version = f"{sys.version_info.major}.{sys.version_info.minor}" + message = f"Test app. From Uvicorn with Gunicorn. Using Python {version}".encode( + "utf-8" + ) + await send({"type": "http.response.body", "body": message}) + + +app = App diff --git a/tests/test_04_app_reload/simple_app/latest.dockerfile b/tests/test_04_app_reload/simple_app/latest.dockerfile new file mode 100644 index 00000000..e24982e0 --- /dev/null +++ b/tests/test_04_app_reload/simple_app/latest.dockerfile @@ -0,0 +1,2 @@ +FROM tiangolo/uvicorn-gunicorn:latest +COPY ./app /app diff --git a/tests/test_04_app_reload/simple_app/python3.6-alpine3.8.dockerfile b/tests/test_04_app_reload/simple_app/python3.6-alpine3.8.dockerfile new file mode 100644 index 00000000..4ca240f4 --- /dev/null +++ b/tests/test_04_app_reload/simple_app/python3.6-alpine3.8.dockerfile @@ -0,0 +1,3 @@ +FROM tiangolo/uvicorn-gunicorn:python3.6-alpine3.8 + +COPY ./app /app diff --git a/tests/test_04_app_reload/simple_app/python3.6.dockerfile b/tests/test_04_app_reload/simple_app/python3.6.dockerfile new file mode 100644 index 00000000..5373fb6b --- /dev/null +++ b/tests/test_04_app_reload/simple_app/python3.6.dockerfile @@ -0,0 +1,3 @@ +FROM tiangolo/uvicorn-gunicorn:python3.6 + +COPY ./app /app diff --git a/tests/test_04_app_reload/simple_app/python3.7-alpine3.8.dockerfile b/tests/test_04_app_reload/simple_app/python3.7-alpine3.8.dockerfile new file mode 100644 index 00000000..4c4e7769 --- /dev/null +++ b/tests/test_04_app_reload/simple_app/python3.7-alpine3.8.dockerfile @@ -0,0 +1,3 @@ +FROM tiangolo/uvicorn-gunicorn:python3.7-alpine3.8 + +COPY ./app /app diff --git a/tests/test_04_app_reload/simple_app/python3.7.dockerfile b/tests/test_04_app_reload/simple_app/python3.7.dockerfile new file mode 100644 index 00000000..b9be60e5 --- /dev/null +++ b/tests/test_04_app_reload/simple_app/python3.7.dockerfile @@ -0,0 +1,3 @@ +FROM tiangolo/uvicorn-gunicorn:python3.7 + +COPY ./app /app diff --git a/tests/test_04_app_reload/test_custom_app.py b/tests/test_04_app_reload/test_custom_app.py new file mode 100644 index 00000000..0a64e0dd --- /dev/null +++ b/tests/test_04_app_reload/test_custom_app.py @@ -0,0 +1,110 @@ +import time +from pathlib import Path, PurePath + +import docker +import pytest +import requests + +from ..utils import ( + CONTAINER_NAME, + IMAGE_NAME, + get_config, + get_logs, + remove_previous_container, +) + +client = docker.from_env() + + +def verify_container(container, response_text): + logs = get_logs(container) + assert "Checking for script in /app/prestart.sh" in logs + assert "Running script /app/prestart.sh" in logs + assert ( + "Running inside /app/prestart.sh, you could add migrations to this file" in logs + ) + assert "Uvicorn running on http://0.0.0.0:80" in logs + response = requests.get("http://127.0.0.1:8000") + assert response.text == response_text + + +@pytest.mark.parametrize( + "dockerfile,environment,response_text", + [ + ( + "python3.6.dockerfile", + {"MODULE_NAME": "custom_app.custom_main", "VARIABLE_NAME": "custom_var"}, + "Test app. From Uvicorn with Gunicorn. Using Python 3.6", + ), + ( + "python3.7.dockerfile", + {"MODULE_NAME": "custom_app.custom_main", "VARIABLE_NAME": "custom_var"}, + "Test app. From Uvicorn with Gunicorn. Using Python 3.7", + ), + ( + "latest.dockerfile", + {"MODULE_NAME": "custom_app.custom_main", "VARIABLE_NAME": "custom_var"}, + "Test app. From Uvicorn with Gunicorn. Using Python 3.7", + ), + ( + "python3.6-alpine3.8.dockerfile", + {"MODULE_NAME": "custom_app.custom_main", "VARIABLE_NAME": "custom_var"}, + "Test app. From Uvicorn with Gunicorn. Using Python 3.6", + ), + ( + "python3.7-alpine3.8.dockerfile", + {"MODULE_NAME": "custom_app.custom_main", "VARIABLE_NAME": "custom_var"}, + "Test app. From Uvicorn with Gunicorn. Using Python 3.7", + ), + ( + "python3.6.dockerfile", + {"APP_MODULE": "custom_app.custom_main:custom_var"}, + "Test app. From Uvicorn with Gunicorn. Using Python 3.6", + ), + ( + "python3.7.dockerfile", + {"APP_MODULE": "custom_app.custom_main:custom_var"}, + "Test app. From Uvicorn with Gunicorn. Using Python 3.7", + ), + ( + "latest.dockerfile", + {"APP_MODULE": "custom_app.custom_main:custom_var"}, + "Test app. From Uvicorn with Gunicorn. Using Python 3.7", + ), + ( + "python3.6-alpine3.8.dockerfile", + {"APP_MODULE": "custom_app.custom_main:custom_var"}, + "Test app. From Uvicorn with Gunicorn. Using Python 3.6", + ), + ( + "python3.7-alpine3.8.dockerfile", + {"APP_MODULE": "custom_app.custom_main:custom_var"}, + "Test app. From Uvicorn with Gunicorn. Using Python 3.7", + ), + ], +) +def test_custom_app(dockerfile, environment, response_text): + remove_previous_container(client) + test_path: PurePath = Path(__file__) + path = test_path.parent / "custom_app" + client.images.build(path=str(path), dockerfile=dockerfile, tag=IMAGE_NAME) + container = client.containers.run( + IMAGE_NAME, + name=CONTAINER_NAME, + environment=environment, + ports={"80": "8000"}, + detach=True, + command="/start-reload.sh", + ) + time.sleep(1) + verify_container(container, response_text) + container.exec_run( + "sed -i 's|Uvicorn with Gunicorn|Uvicorn with autoreload|' /app/custom_app/custom_main.py" + ) + new_response_text = response_text.replace( + "Uvicorn with Gunicorn", "Uvicorn with autoreload" + ) + time.sleep(1) + verify_container(container, new_response_text) + container.stop() + container.remove() diff --git a/tests/test_04_app_reload/test_package_app.py b/tests/test_04_app_reload/test_package_app.py new file mode 100644 index 00000000..ae0bbd2f --- /dev/null +++ b/tests/test_04_app_reload/test_package_app.py @@ -0,0 +1,75 @@ +import time +from pathlib import Path, PurePath + +import docker +import pytest +import requests + +from ..utils import ( + CONTAINER_NAME, + IMAGE_NAME, + get_config, + get_logs, + remove_previous_container, +) + +client = docker.from_env() + + +def verify_container(container, response_text): + config_data = get_config(container) + assert config_data["workers_per_core"] == 1 + assert config_data["host"] == "0.0.0.0" + assert config_data["port"] == "80" + assert config_data["loglevel"] == "info" + assert config_data["workers"] >= 2 + assert config_data["bind"] == "0.0.0.0:80" + logs = get_logs(container) + assert "Checking for script in /app/prestart.sh" in logs + assert "Running script /app/prestart.sh" in logs + assert ( + "Running inside /app/prestart.sh, you could add migrations to this file" in logs + ) + response = requests.get("http://127.0.0.1:8000") + assert response.text == response_text + + +@pytest.mark.parametrize( + "dockerfile,response_text", + [ + ( + "python3.6.dockerfile", + "Test app. From Uvicorn with Gunicorn. Using Python 3.6", + ), + ( + "python3.7.dockerfile", + "Test app. From Uvicorn with Gunicorn. Using Python 3.7", + ), + ("latest.dockerfile", "Test app. From Uvicorn with Gunicorn. Using Python 3.7"), + ( + "python3.6-alpine3.8.dockerfile", + "Test app. From Uvicorn with Gunicorn. Using Python 3.6", + ), + ( + "python3.7-alpine3.8.dockerfile", + "Test app. From Uvicorn with Gunicorn. Using Python 3.7", + ), + ], +) +def test_package_app(dockerfile, response_text): + remove_previous_container(client) + test_path: PurePath = Path(__file__) + path = test_path.parent / "package_app" + client.images.build(path=str(path), dockerfile=dockerfile, tag=IMAGE_NAME) + container = client.containers.run( + IMAGE_NAME, name=CONTAINER_NAME, ports={"80": "8000"}, detach=True + ) + time.sleep(1) + verify_container(container, response_text) + container.stop() + # Test that everything works after restarting too + container.start() + time.sleep(1) + verify_container(container, response_text) + container.stop() + container.remove() diff --git a/tests/test_04_app_reload/test_simple_app.py b/tests/test_04_app_reload/test_simple_app.py new file mode 100644 index 00000000..a848e39f --- /dev/null +++ b/tests/test_04_app_reload/test_simple_app.py @@ -0,0 +1,73 @@ +import time +from pathlib import Path, PurePath + +import docker +import pytest +import requests + +from ..utils import ( + CONTAINER_NAME, + IMAGE_NAME, + get_config, + get_logs, + remove_previous_container, +) + +client = docker.from_env() + + +def verify_container(container, response_text): + logs = get_logs(container) + assert "Checking for script in /app/prestart.sh" in logs + assert "Running script /app/prestart.sh" in logs + assert ( + "Running inside /app/prestart.sh, you could add migrations to this file" in logs + ) + assert "Uvicorn running on http://0.0.0.0:80" in logs + response = requests.get("http://127.0.0.1:8000") + assert response.text == response_text + + +@pytest.mark.parametrize( + "dockerfile,response_text", + [ + ( + "python3.6.dockerfile", + "Test app. From Uvicorn with Gunicorn. Using Python 3.6", + ), + ( + "python3.7.dockerfile", + "Test app. From Uvicorn with Gunicorn. Using Python 3.7", + ), + ("latest.dockerfile", "Test app. From Uvicorn with Gunicorn. Using Python 3.7"), + ( + "python3.6-alpine3.8.dockerfile", + "Test app. From Uvicorn with Gunicorn. Using Python 3.6", + ), + ( + "python3.7-alpine3.8.dockerfile", + "Test app. From Uvicorn with Gunicorn. Using Python 3.7", + ), + ], +) +def test_simple_app(dockerfile, response_text): + remove_previous_container(client) + IMAGE_NAME + test_path: PurePath = Path(__file__) + path = test_path.parent / "simple_app" + client.images.build(path=str(path), dockerfile=dockerfile, tag=IMAGE_NAME) + container = client.containers.run( + IMAGE_NAME, name=CONTAINER_NAME, ports={"80": "8000"}, detach=True, command="/start-reload.sh", + ) + time.sleep(1) + verify_container(container, response_text) + container.exec_run( + "sed -i 's|Uvicorn with Gunicorn|Uvicorn with autoreload|' /app/custom_app/custom_main.py" + ) + new_response_text = response_text.replace( + "Uvicorn with Gunicorn", "Uvicorn with autoreload" + ) + time.sleep(1) + verify_container(container, new_response_text) + container.stop() + container.remove() From 25146f79357f7f9dc94616f338364d4b39a57d8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 7 Mar 2019 16:18:50 +0400 Subject: [PATCH 5/5] :bug: Fix testing, live reload change correct file --- tests/test_04_app_reload/test_simple_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_04_app_reload/test_simple_app.py b/tests/test_04_app_reload/test_simple_app.py index a848e39f..f67573cd 100644 --- a/tests/test_04_app_reload/test_simple_app.py +++ b/tests/test_04_app_reload/test_simple_app.py @@ -62,7 +62,7 @@ def test_simple_app(dockerfile, response_text): time.sleep(1) verify_container(container, response_text) container.exec_run( - "sed -i 's|Uvicorn with Gunicorn|Uvicorn with autoreload|' /app/custom_app/custom_main.py" + "sed -i 's|Uvicorn with Gunicorn|Uvicorn with autoreload|' /app/main.py" ) new_response_text = response_text.replace( "Uvicorn with Gunicorn", "Uvicorn with autoreload"