From 35e46014926b0c43c40cdd08b5d1bbab00f674b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 27 Apr 2020 19:20:37 +0200 Subject: [PATCH] :sparkles: Refactor update from base image (#16) * :fire: Refactor code to simplify and deduplicate * :construction_worker: Add GitHub Actions for CI * :wrench: Update build and test scripts * :white_check_mark: Update tests * :heavy_plus_sign: Add dev dependencies * :memo: Update docs with new configs * :white_check_mark: Fix tests --- .github/workflows/deploy.yml | 44 ++++++ .github/workflows/issue-manager.yml | 21 +++ .github/workflows/test.yml | 42 ++++++ .travis.yml | 34 ----- README.md | 142 +++++++++++++++++- backup.travis.yml | 35 +++++ {python3.6 => docker-images}/app/main.py | 0 .../python3.6-alpine3.8.dockerfile | 2 +- .../python3.6.dockerfile | 2 +- .../python3.7-alpine3.8.dockerfile | 2 +- .../python3.7.dockerfile | 2 +- docker-images/python3.8-alpine3.10.dockerfile | 7 + docker-images/python3.8.dockerfile | 7 + mypy.ini | 3 + pyproject.toml | 4 + python3.6-alpine3.8/app/main.py | 14 -- python3.7-alpine3.8/app/main.py | 14 -- python3.7/app/main.py | 14 -- scripts/build-push-all.sh | 2 +- scripts/build-push.sh | 4 +- scripts/build.sh | 12 ++ scripts/docker-login.sh | 5 + scripts/format-imports.sh | 6 + scripts/format.sh | 7 + scripts/lint.sh | 7 +- scripts/process_all.py | 32 +--- scripts/test.sh | 4 +- tests/test_01_main/test_defaults.py | 31 +++- tests/utils.py | 21 ++- 29 files changed, 386 insertions(+), 134 deletions(-) create mode 100644 .github/workflows/deploy.yml create mode 100644 .github/workflows/issue-manager.yml create mode 100644 .github/workflows/test.yml delete mode 100644 .travis.yml create mode 100644 backup.travis.yml rename {python3.6 => docker-images}/app/main.py (100%) rename python3.6-alpine3.8/Dockerfile => docker-images/python3.6-alpine3.8.dockerfile (75%) rename python3.6/Dockerfile => docker-images/python3.6.dockerfile (74%) rename python3.7-alpine3.8/Dockerfile => docker-images/python3.7-alpine3.8.dockerfile (75%) rename python3.7/Dockerfile => docker-images/python3.7.dockerfile (74%) create mode 100644 docker-images/python3.8-alpine3.10.dockerfile create mode 100644 docker-images/python3.8.dockerfile create mode 100644 mypy.ini delete mode 100755 python3.6-alpine3.8/app/main.py delete mode 100755 python3.7-alpine3.8/app/main.py delete mode 100755 python3.7/app/main.py create mode 100644 scripts/build.sh create mode 100644 scripts/docker-login.sh create mode 100755 scripts/format-imports.sh create mode 100644 scripts/format.sh mode change 100644 => 100755 scripts/lint.sh diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..8ca6f68 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,44 @@ +name: Deploy + +on: + push: + branches: + - master + +jobs: + build: + strategy: + matrix: + image: + - name: latest + python_version: "3.8" + - name: python3.8 + python_version: "3.8" + - name: python3.7 + python_version: "3.7" + - name: python3.6 + python_version: "3.6" + - name: python3.8-alpine3.10 + python_version: "3.8" + - name: python3.7-alpine3.8 + python_version: "3.7" + - name: python3.6-alpine3.8 + python_version: "3.6" + fail-fast: true + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: "3.7" + - name: Install Dependencies + run: python3.7 -m pip install docker pytest + - name: Deploy Image + run: bash scripts/build-push.sh + env: + NAME: ${{ matrix.image.name }} + DOCKERFILE: ${{ matrix.image.dockerfile }} + PYTHON_VERSION: ${{ matrix.image.python_version }} + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml new file mode 100644 index 0000000..cd5d7a0 --- /dev/null +++ b/.github/workflows/issue-manager.yml @@ -0,0 +1,21 @@ +name: Issue Manager + +on: + schedule: + - cron: "0 0 * * *" + +jobs: + issue-manager: + runs-on: ubuntu-latest + steps: + - uses: tiangolo/issue-manager@master + with: + token: ${{ secrets.GITHUB_TOKEN }} + config: > + { + "answered": { + "users": ["tiangolo"], + "delay": 864000, + "message": "Assuming the original issue was solved, it will be automatically closed now. But feel free to add more comments or create new issues." + } + } diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..4d7923f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,42 @@ +name: Test + +on: + push: + pull_request: + types: [opened, synchronize] + +jobs: + build: + strategy: + matrix: + image: + - name: latest + python_version: "3.8" + - name: python3.8 + python_version: "3.8" + - name: python3.7 + python_version: "3.7" + - name: python3.6 + python_version: "3.6" + - name: python3.8-alpine3.10 + python_version: "3.8" + - name: python3.7-alpine3.8 + python_version: "3.7" + - name: python3.6-alpine3.8 + python_version: "3.6" + fail-fast: true + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: "3.7" + - name: Install Dependencies + run: python3.7 -m pip install docker pytest + - name: Test Image + run: bash scripts/test.sh + env: + NAME: ${{ matrix.image.name }} + DOCKERFILE: ${{ matrix.image.dockerfile }} + PYTHON_VERSION: ${{ matrix.image.python_version }} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c14b5fb..0000000 --- a/.travis.yml +++ /dev/null @@ -1,34 +0,0 @@ -dist: xenial - -language: python - -python: - - "3.7" - -install: - - pip install docker pytest - -services: - - docker - -env: - - NAME='latest' BUILD_PATH='python3.7' TEST_STR1='Hello world! From Starlette running on Uvicorn with Gunicorn. Using Python 3.7' - - NAME='python3.7' BUILD_PATH='python3.7' TEST_STR1='Hello world! From Starlette running on Uvicorn with Gunicorn. Using Python 3.7' - - NAME='python3.6' BUILD_PATH='python3.6' TEST_STR1='Hello world! From Starlette running on Uvicorn with Gunicorn. Using Python 3.6' - - NAME='python3.7-alpine3.8' BUILD_PATH='python3.7-alpine3.8' TEST_STR1='Hello world! From Starlette running on Uvicorn with Gunicorn in Alpine. Using Python 3.7' - - NAME='python3.6-alpine3.8' BUILD_PATH='python3.6-alpine3.8' TEST_STR1='Hello world! From Starlette running on Uvicorn with Gunicorn in Alpine. Using Python 3.6' - - -script: - - bash scripts/test.sh - -jobs: - include: - - script: bash scripts/test.sh - - stage: deploy - script: skip - deploy: - provider: script - script: bash scripts/build-push-all.sh - on: - branch: master diff --git a/README.md b/README.md index 9c0b942..df503fc 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,19 @@ -[![Build Status](https://travis-ci.com/tiangolo/uvicorn-gunicorn-starlette-docker.svg?branch=master)](https://travis-ci.com/tiangolo/uvicorn-gunicorn-starlette-docker) +[![Test](https://github.com/tiangolo/uvicorn-gunicorn-starlette-docker/workflows/Test/badge.svg)](https://github.com/tiangolo/uvicorn-gunicorn-starlette-docker/actions?query=workflow%3ATest) [![Deploy](https://github.com/tiangolo/uvicorn-gunicorn-starlette-docker/workflows/Deploy/badge.svg)](https://github.com/tiangolo/uvicorn-gunicorn-starlette-docker/actions?query=workflow%3ADeploy) ## Supported tags and respective `Dockerfile` links -* [`python3.7`, `latest` _(Dockerfile)_](https://github.com/tiangolo/uvicorn-gunicorn-starlette-docker/blob/master/python3.7/Dockerfile) -* [`python3.6` _(Dockerfile)_](https://github.com/tiangolo/uvicorn-gunicorn-starlette-docker/blob/master/python3.6/Dockerfile) -* [`python3.6-alpine3.8` _(Dockerfile)_](https://github.com/tiangolo/uvicorn-gunicorn-starlette-docker/blob/master/python3.6-alpine3.8/Dockerfile) -* [`python3.7-alpine3.8` _(Dockerfile)_](https://github.com/tiangolo/uvicorn-gunicorn-starlette-docker/blob/master/python3.7-alpine3.8/Dockerfile) +* [`python3.8`, `latest` _(Dockerfile)_](https://github.com/tiangolo/uvicorn-gunicorn-starlette-docker/blob/master/docker-images/python3.8.dockerfile) +* [`python3.7`, _(Dockerfile)_](https://github.com/tiangolo/uvicorn-gunicorn-starlette-docker/blob/master/docker-images/python3.7.dockerfile) +* [`python3.6` _(Dockerfile)_](https://github.com/tiangolo/uvicorn-gunicorn-starlette-docker/blob/master/docker-images/python3.6.dockerfile) +* [`python3.8-alpine3.10` _(Dockerfile)_](https://github.com/tiangolo/uvicorn-gunicorn-starlette-docker/blob/master/docker-images/python3.8-alpine3.10.dockerfile) +* [`python3.7-alpine3.8` _(Dockerfile)_](https://github.com/tiangolo/uvicorn-gunicorn-starlette-docker/blob/master/docker-images/python3.7-alpine3.8.dockerfile) +* [`python3.6-alpine3.8` _(Dockerfile)_](https://github.com/tiangolo/uvicorn-gunicorn-starlette-docker/blob/master/docker-images/python3.6-alpine3.8.dockerfile) **Note**: Note: There are [tags for each build date](https://hub.docker.com/r/tiangolo/uvicorn-gunicorn-starlette/tags). If you need to "pin" the Docker image version you use, you can select one of those tags. E.g. `tiangolo/uvicorn-gunicorn-starlette:python3.7-2019-10-15`. # uvicorn-gunicorn-starlette -[**Docker**](https://www.docker.com/) image with [**Uvicorn**](https://www.uvicorn.org/) managed by [**Gunicorn**](https://gunicorn.org/) for high-performance [**Starlette**](https://www.starlette.io/) web applications in **[Python](https://www.python.org/) 3.7** and **3.6** with performance auto-tuning. Optionally with Alpine Linux. +[**Docker**](https://www.docker.com/) image with [**Uvicorn**](https://www.uvicorn.org/) managed by [**Gunicorn**](https://gunicorn.org/) for high-performance [**Starlette**](https://www.starlette.io/) web applications in **[Python](https://www.python.org/) 3.6 and above** with performance auto-tuning. Optionally with Alpine Linux. **GitHub repo**: [https://github.com/tiangolo/uvicorn-gunicorn-starlette-docker](https://github.com/tiangolo/uvicorn-gunicorn-starlette-docker) @@ -229,6 +231,8 @@ You can set it like: docker run -d -p 80:80 -e GUNICORN_CONF="/app/custom_gunicorn_conf.py" myimage ``` +You can use the [config file from the base image](https://github.com/tiangolo/uvicorn-gunicorn-docker/blob/master/docker-images/gunicorn_conf.py) as a starting point for yours. + #### `WORKERS_PER_CORE` This image will check how many CPU cores are available in the current server running your container. @@ -259,6 +263,24 @@ In a server with 8 CPU cores, this would make it start only 4 worker processes. **Note**: By default, if `WORKERS_PER_CORE` is `1` and the server has only 1 CPU core, instead of starting 1 single worker, it will start 2. This is to avoid bad performance and blocking applications (server application) on small machines (server machine/cloud/etc). This can be overridden using `WEB_CONCURRENCY`. +#### `MAX_WORKERS` + +Set the maximum number of workers to use. + +You can use it to let the image compute the number of workers automatically but making sure it's limited to a maximum. + +This can be useful, for example, if each worker uses a database connection and your database has a maximum limit of open connections. + +By default it's not set, meaning that it's unlimited. + +You can set it like: + +```bash +docker run -d -p 80:80 -e MAX_WORKERS="24" myimage +``` + +This would make the image start at most 24 workers, independent of how many CPU cores are available in the server. + #### `WEB_CONCURRENCY` Override the automatic definition of number of workers. @@ -343,6 +365,112 @@ You can set it like: docker run -d -p 80:8080 -e LOG_LEVEL="warning" myimage ``` +#### `WORKER_CLASS` + +The class to be used by Gunicorn for the workers. + +By default, set to `uvicorn.workers.UvicornWorker`. + +The fact that it uses Uvicorn is what allows using ASGI frameworks like Starlette, and that is also what provides the maximum performance. + +You probably shouldn't change it. + +But if for some reason you need to use the alternative Uvicorn worker: `uvicorn.workers.UvicornH11Worker` you can set it with this environment variable. + +You can set it like: + +```bash +docker run -d -p 80:8080 -e WORKER_CLASS="uvicorn.workers.UvicornH11Worker" myimage +``` + +#### `TIMEOUT` + +Workers silent for more than this many seconds are killed and restarted. + +Read more about it in the [Gunicorn docs: timeout](https://docs.gunicorn.org/en/stable/settings.html#timeout). + +By default, set to `120`. + +Notice that Uvicorn and ASGI frameworks like Starlette are async, not sync. So it's probably safe to have higher timeouts than for sync workers. + +You can set it like: + +```bash +docker run -d -p 80:8080 -e TIMEOUT="20" myimage +``` + +#### `KEEP_ALIVE` + +The number of seconds to wait for requests on a Keep-Alive connection. + +Read more about it in the [Gunicorn docs: keepalive](https://docs.gunicorn.org/en/stable/settings.html#keepalive). + +By default, set to `2`. + +You can set it like: + +```bash +docker run -d -p 80:8080 -e KEEP_ALIVE="20" myimage +``` + +#### `GRACEFUL_TIMEOUT` + +Timeout for graceful workers restart. + +Read more about it in the [Gunicorn docs: graceful-timeout](https://docs.gunicorn.org/en/stable/settings.html#graceful-timeout). + +By default, set to `120`. + +You can set it like: + +```bash +docker run -d -p 80:8080 -e GRACEFUL_TIMEOUT="20" myimage +``` + +#### `ACCESS_LOG` + +The access log file to write to. + +By default `"-"`, which means stdout (print in the Docker logs). + +If you want to disable `ACCESS_LOG`, set it to an empty value. + +For example, you could disable it with: + +```bash +docker run -d -p 80:8080 -e ACCESS_LOG= myimage +``` + +#### `ERROR_LOG` + +The error log file to write to. + +By default `"-"`, which means stderr (print in the Docker logs). + +If you want to disable `ERROR_LOG`, set it to an empty value. + +For example, you could disable it with: + +```bash +docker run -d -p 80:8080 -e ERROR_LOG= myimage +``` + +#### `GUNICORN_CMD_ARGS` + +Any additional command line settings for Gunicorn can be passed in the `GUNICORN_CMD_ARGS` environment variable. + +Read more about it in the [Gunicorn docs: Settings](https://docs.gunicorn.org/en/stable/settings.html#settings). + +These settings will have precedence over the other environment variables and any Gunicorn config file. + +For example, if you have a custom TLS/SSL certificate that you want to use, you could copy them to the Docker image or mount them in the container, and set [`--keyfile` and `--certfile`](http://docs.gunicorn.org/en/latest/settings.html#ssl) to the location of the files, for example: + +```bash +docker run -d -p 80:8080 -e GUNICORN_CMD_ARGS="--keyfile=/secrets/key.pem --certfile=/secrets/cert.pem" -e PORT=443 myimage +``` + +**Note**: instead of handling TLS/SSL yourself and configuring it in the container, it's recommended to use a "TLS Termination Proxy" like [Traefik](https://docs.traefik.io/). You can read more about it in the [FastAPI documentation about HTTPS](https://fastapi.tiangolo.com/deployment/#https). + #### `PRE_START_PATH` The path where to find the pre-start script. @@ -433,7 +561,7 @@ docker run -d -p 80:80 -v $(pwd):/app myimage /start-reload.sh * `$(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 +#### Development live reload - 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. diff --git a/backup.travis.yml b/backup.travis.yml new file mode 100644 index 0000000..8146cab --- /dev/null +++ b/backup.travis.yml @@ -0,0 +1,35 @@ +dist: xenial + +language: python + +python: + - "3.7" + +install: + - pip install docker pytest + +services: + - docker + +env: + - NAME='latest' PYTHON_VERSION='3.8' + - NAME='python3.8' PYTHON_VERSION='3.8' + - NAME='python3.7' PYTHON_VERSION='3.7' + - NAME='python3.6' PYTHON_VERSION='3.6' + - NAME='python3.8-alpine3.10' PYTHON_VERSION='3.8' + - NAME='python3.7-alpine3.8' PYTHON_VERSION='3.7' + - NAME='python3.6-alpine3.8' PYTHON_VERSION='3.6' + +script: + - bash scripts/test.sh + +jobs: + include: + - script: bash scripts/test.sh + - stage: deploy + script: skip + deploy: + provider: script + script: bash scripts/build-push-all.sh + on: + branch: master diff --git a/python3.6/app/main.py b/docker-images/app/main.py similarity index 100% rename from python3.6/app/main.py rename to docker-images/app/main.py diff --git a/python3.6-alpine3.8/Dockerfile b/docker-images/python3.6-alpine3.8.dockerfile similarity index 75% rename from python3.6-alpine3.8/Dockerfile rename to docker-images/python3.6-alpine3.8.dockerfile index c62bb53..d066c6c 100644 --- a/python3.6-alpine3.8/Dockerfile +++ b/docker-images/python3.6-alpine3.8.dockerfile @@ -2,6 +2,6 @@ FROM tiangolo/uvicorn-gunicorn:python3.6-alpine3.8 LABEL maintainer="Sebastian Ramirez " -RUN pip --no-cache-dir install starlette +RUN pip install --no-cache-dir starlette COPY ./app /app diff --git a/python3.6/Dockerfile b/docker-images/python3.6.dockerfile similarity index 74% rename from python3.6/Dockerfile rename to docker-images/python3.6.dockerfile index cdcbc08..d7c57bc 100644 --- a/python3.6/Dockerfile +++ b/docker-images/python3.6.dockerfile @@ -2,6 +2,6 @@ FROM tiangolo/uvicorn-gunicorn:python3.6 LABEL maintainer="Sebastian Ramirez " -RUN pip --no-cache-dir install starlette +RUN pip install --no-cache-dir starlette COPY ./app /app diff --git a/python3.7-alpine3.8/Dockerfile b/docker-images/python3.7-alpine3.8.dockerfile similarity index 75% rename from python3.7-alpine3.8/Dockerfile rename to docker-images/python3.7-alpine3.8.dockerfile index f7e9bf2..ffa7423 100644 --- a/python3.7-alpine3.8/Dockerfile +++ b/docker-images/python3.7-alpine3.8.dockerfile @@ -2,6 +2,6 @@ FROM tiangolo/uvicorn-gunicorn:python3.7-alpine3.8 LABEL maintainer="Sebastian Ramirez " -RUN pip --no-cache-dir install starlette +RUN pip install --no-cache-dir starlette COPY ./app /app diff --git a/python3.7/Dockerfile b/docker-images/python3.7.dockerfile similarity index 74% rename from python3.7/Dockerfile rename to docker-images/python3.7.dockerfile index 3899c50..bd81bcb 100644 --- a/python3.7/Dockerfile +++ b/docker-images/python3.7.dockerfile @@ -2,6 +2,6 @@ FROM tiangolo/uvicorn-gunicorn:python3.7 LABEL maintainer="Sebastian Ramirez " -RUN pip --no-cache-dir install starlette +RUN pip install --no-cache-dir starlette COPY ./app /app diff --git a/docker-images/python3.8-alpine3.10.dockerfile b/docker-images/python3.8-alpine3.10.dockerfile new file mode 100644 index 0000000..af7a58d --- /dev/null +++ b/docker-images/python3.8-alpine3.10.dockerfile @@ -0,0 +1,7 @@ +FROM tiangolo/uvicorn-gunicorn:python3.8-alpine3.10 + +LABEL maintainer="Sebastian Ramirez " + +RUN pip install --no-cache-dir starlette + +COPY ./app /app diff --git a/docker-images/python3.8.dockerfile b/docker-images/python3.8.dockerfile new file mode 100644 index 0000000..d2607b2 --- /dev/null +++ b/docker-images/python3.8.dockerfile @@ -0,0 +1,7 @@ +FROM tiangolo/uvicorn-gunicorn:python3.8 + +LABEL maintainer="Sebastian Ramirez " + +RUN pip install --no-cache-dir starlette + +COPY ./app /app diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..4ff4483 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,3 @@ +[mypy] +disallow_untyped_defs = True +ignore_missing_imports = True diff --git a/pyproject.toml b/pyproject.toml index 5c240ed..85b56d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,10 @@ docker = "^4.2.0" pytest = "^5.4.1" [tool.poetry.dev-dependencies] +black = "^19.10b0" +isort = "^4.3.21" +autoflake = "^1.3.1" +mypy = "^0.770" [build-system] requires = ["poetry>=0.12"] diff --git a/python3.6-alpine3.8/app/main.py b/python3.6-alpine3.8/app/main.py deleted file mode 100755 index 1fe71c3..0000000 --- a/python3.6-alpine3.8/app/main.py +++ /dev/null @@ -1,14 +0,0 @@ -import sys - -from starlette.applications import Starlette -from starlette.responses import JSONResponse - -version = f"{sys.version_info.major}.{sys.version_info.minor}" - -app = Starlette() - - -@app.route("/") -async def homepage(request): - message = f"Hello world! From Starlette running on Uvicorn with Gunicorn in Alpine. Using Python {version}" - return JSONResponse({"message": message}) diff --git a/python3.7-alpine3.8/app/main.py b/python3.7-alpine3.8/app/main.py deleted file mode 100755 index 1fe71c3..0000000 --- a/python3.7-alpine3.8/app/main.py +++ /dev/null @@ -1,14 +0,0 @@ -import sys - -from starlette.applications import Starlette -from starlette.responses import JSONResponse - -version = f"{sys.version_info.major}.{sys.version_info.minor}" - -app = Starlette() - - -@app.route("/") -async def homepage(request): - message = f"Hello world! From Starlette running on Uvicorn with Gunicorn in Alpine. Using Python {version}" - return JSONResponse({"message": message}) diff --git a/python3.7/app/main.py b/python3.7/app/main.py deleted file mode 100755 index 427ecb2..0000000 --- a/python3.7/app/main.py +++ /dev/null @@ -1,14 +0,0 @@ -import sys - -from starlette.applications import Starlette -from starlette.responses import JSONResponse - -version = f"{sys.version_info.major}.{sys.version_info.minor}" - -app = Starlette() - - -@app.route("/") -async def homepage(request): - message = f"Hello world! From Starlette running on Uvicorn with Gunicorn. Using Python {version}" - return JSONResponse({"message": message}) diff --git a/scripts/build-push-all.sh b/scripts/build-push-all.sh index e54d3f1..a18cab7 100644 --- a/scripts/build-push-all.sh +++ b/scripts/build-push-all.sh @@ -2,6 +2,6 @@ set -e -echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin +bash scripts/docker-login.sh BUILD_PUSH=1 python scripts/process_all.py diff --git a/scripts/build-push.sh b/scripts/build-push.sh index d7655b3..d01971f 100644 --- a/scripts/build-push.sh +++ b/scripts/build-push.sh @@ -5,9 +5,11 @@ set -e use_tag="tiangolo/uvicorn-gunicorn-starlette:$NAME" use_dated_tag="${use_tag}-$(date -I)" -docker build -t "$use_tag" "$BUILD_PATH" +bash scripts/build.sh docker tag "$use_tag" "$use_dated_tag" +bash scripts/docker-login.sh + docker push "$use_tag" docker push "$use_dated_tag" diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 0000000..63a786f --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -e + +use_tag="tiangolo/uvicorn-gunicorn-starlette:$NAME" + +DOCKERFILE="$NAME" + +if [ "$NAME" == "latest" ] ; then + DOCKERFILE="python3.8" +fi + +docker build -t "$use_tag" --file "./docker-images/${DOCKERFILE}.dockerfile" "./docker-images/" diff --git a/scripts/docker-login.sh b/scripts/docker-login.sh new file mode 100644 index 0000000..d872c89 --- /dev/null +++ b/scripts/docker-login.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -e + +echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin diff --git a/scripts/format-imports.sh b/scripts/format-imports.sh new file mode 100755 index 0000000..8710c79 --- /dev/null +++ b/scripts/format-imports.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -x + +# Sort imports one per line, so autoflake can remove unused imports +isort --recursive --force-single-line-imports --apply ./ +sh ./scripts/format.sh diff --git a/scripts/format.sh b/scripts/format.sh new file mode 100644 index 0000000..a38f39a --- /dev/null +++ b/scripts/format.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -x + +autoflake --remove-all-unused-imports --recursive --remove-unused-variables --in-place ./ --exclude=__init__.py +isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --combine-as --line-width 88 --recursive --apply ./ +black ./ diff --git a/scripts/lint.sh b/scripts/lint.sh old mode 100644 new mode 100755 index a38f39a..8198da0 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash +set -e set -x -autoflake --remove-all-unused-imports --recursive --remove-unused-variables --in-place ./ --exclude=__init__.py -isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --combine-as --line-width 88 --recursive --apply ./ -black ./ +mypy ./ +black ./ --check +isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --combine-as --line-width 88 --recursive --check-only diff --git a/scripts/process_all.py b/scripts/process_all.py index 1aa3a60..12b084d 100644 --- a/scripts/process_all.py +++ b/scripts/process_all.py @@ -3,31 +3,13 @@ import sys environments = [ - { - "NAME": "latest", - "BUILD_PATH": "python3.7", - "TEST_STR1": "Hello world! From Starlette running on Uvicorn with Gunicorn. Using Python 3.7", - }, - { - "NAME": "python3.7", - "BUILD_PATH": "python3.7", - "TEST_STR1": "Hello world! From Starlette running on Uvicorn with Gunicorn. Using Python 3.7", - }, - { - "NAME": "python3.6", - "BUILD_PATH": "python3.6", - "TEST_STR1": "Hello world! From Starlette running on Uvicorn with Gunicorn. Using Python 3.6", - }, - { - "NAME": "python3.7-alpine3.8", - "BUILD_PATH": "python3.7-alpine3.8", - "TEST_STR1": "Hello world! From Starlette running on Uvicorn with Gunicorn in Alpine. Using Python 3.7", - }, - { - "NAME": "python3.6-alpine3.8", - "BUILD_PATH": "python3.6-alpine3.8", - "TEST_STR1": "Hello world! From Starlette running on Uvicorn with Gunicorn in Alpine. Using Python 3.6", - }, + {"NAME": "latest", "PYTHON_VERSION": "3.8"}, + {"NAME": "python3.8", "PYTHON_VERSION": "3.8"}, + {"NAME": "python3.7", "PYTHON_VERSION": "3.7"}, + {"NAME": "python3.6", "PYTHON_VERSION": "3.6"}, + {"NAME": "python3.8-alpine3.10", "PYTHON_VERSION": "3.8"}, + {"NAME": "python3.7-alpine3.8", "PYTHON_VERSION": "3.7"}, + {"NAME": "python3.6-alpine3.8", "PYTHON_VERSION": "3.6"}, ] start_with = os.environ.get("START_WITH") diff --git a/scripts/test.sh b/scripts/test.sh index a8c7f12..693467a 100644 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,7 +1,5 @@ #!/usr/bin/env bash set -e -use_tag="tiangolo/uvicorn-gunicorn-starlette:$NAME" - -docker build -t "$use_tag" "$BUILD_PATH" +bash scripts/build.sh pytest tests diff --git a/tests/test_01_main/test_defaults.py b/tests/test_01_main/test_defaults.py index ca2b531..071dc55 100644 --- a/tests/test_01_main/test_defaults.py +++ b/tests/test_01_main/test_defaults.py @@ -2,37 +2,52 @@ import time import docker -import pytest import requests +from docker.client import DockerClient -from ..utils import CONTAINER_NAME, get_config, get_logs, remove_previous_container +from ..utils import ( + CONTAINER_NAME, + get_config, + get_logs, + get_response_text1, + remove_previous_container, +) client = docker.from_env() -def verify_container(container, response_text): +def verify_container(container: DockerClient, response_text: str) -> None: config_data = get_config(container) assert config_data["workers_per_core"] == 1 + assert config_data["use_max_workers"] is None 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" + assert config_data["graceful_timeout"] == 120 + assert config_data["timeout"] == 120 + assert config_data["keepalive"] == 5 + assert config_data["errorlog"] == "-" + assert config_data["accesslog"] == "-" + response = requests.get("http://127.0.0.1:8000") + data = response.json() + assert data["message"] == 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 ) - response = requests.get("http://127.0.0.1:8000") - data = response.json() - assert data["message"] == response_text + assert '"GET / HTTP/1.1" 200' in logs + assert "[INFO] Application startup complete." in logs + assert "Using worker: uvicorn.workers.UvicornWorker" in logs -def test_defaults(): +def test_defaults() -> None: name = os.getenv("NAME") image = f"tiangolo/uvicorn-gunicorn-starlette:{name}" - response_text = os.getenv("TEST_STR1") + response_text = get_response_text1() sleep_time = int(os.getenv("SLEEP_TIME", 1)) remove_previous_container(client) container = client.containers.run( diff --git a/tests/utils.py b/tests/utils.py index f8b3b9d..2b75d7c 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,18 +1,22 @@ import json +import os +from typing import Any, Dict, List +from docker.client import DockerClient from docker.errors import NotFound +from docker.models.containers import Container CONTAINER_NAME = "uvicorn-gunicorn-starlette-test" -def get_process_names(container): +def get_process_names(container: Container) -> List[str]: top = container.top() process_commands = [p[7] for p in top["Processes"]] gunicorn_processes = [p for p in process_commands if "gunicorn" in p] return gunicorn_processes -def get_gunicorn_conf_path(container): +def get_gunicorn_conf_path(container: Container) -> str: gunicorn_processes = get_process_names(container) first_process = gunicorn_processes[0] first_part, partition, last_part = first_process.partition("-c") @@ -20,13 +24,13 @@ def get_gunicorn_conf_path(container): return gunicorn_conf -def get_config(container): +def get_config(container: Container) -> Dict[str, Any]: gunicorn_conf = get_gunicorn_conf_path(container) result = container.exec_run(f"python {gunicorn_conf}") return json.loads(result.output.decode()) -def remove_previous_container(client): +def remove_previous_container(client: DockerClient) -> None: try: previous = client.containers.get(CONTAINER_NAME) previous.stop() @@ -35,6 +39,11 @@ def remove_previous_container(client): return None -def get_logs(container): - logs: str = container.logs() +def get_logs(container: DockerClient) -> str: + logs = container.logs() return logs.decode("utf-8") + + +def get_response_text1() -> str: + python_version = os.getenv("PYTHON_VERSION") + return f"Hello world! From Starlette running on Uvicorn with Gunicorn. Using Python {python_version}"