From ec3222aef79fe6817c91dccd0cdca7d5d1efc425 Mon Sep 17 00:00:00 2001 From: Jerrico Gamis Date: Tue, 18 Jun 2024 07:10:56 +1000 Subject: [PATCH] Use Ubuntu 24.04 Refactor build info Clean up build --- .github/workflows/build.yml | 4 +- Dockerfile | 21 ++++----- Makefile | 44 +++++++++---------- README.md | 44 ++++++++++--------- app/__init__.py | 8 ++-- app/root.py | 6 +-- docker-entrypoint.sh | 9 +++- ...e-server-info.sh => generate-build-info.sh | 4 +- generate-ssl-certs.sh | 2 +- requirements-dev.txt | 2 - requirements.txt | 7 +-- run-app-dev-mode.sh => run-app.sh | 6 +-- smoke-tests.py | 36 ++++----------- tests/test_root.py | 4 +- 14 files changed, 89 insertions(+), 108 deletions(-) rename generate-server-info.sh => generate-build-info.sh (64%) delete mode 100644 requirements-dev.txt rename run-app-dev-mode.sh => run-app.sh (60%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0d1456d..0d2bd8d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,10 +24,10 @@ jobs: python -m pip install --upgrade pip pip install pytest if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Build server_info, ssl certs + - name: Build build info, ssl certs run: | ./generate-ssl-certs.sh - ./generate-server-info.sh + ./generate-build-info.sh - name: Test with pytest run: | pytest diff --git a/Dockerfile b/Dockerfile index cdbd212..64d34b6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,27 +1,24 @@ -FROM ubuntu:22.04 +FROM ubuntu:24.04 MAINTAINER Jerrico Gamis -RUN apt update -y && apt install -y curl dumb-init python3 python3-pip && rm -rf /var/lib/apt/lists/* -RUN pip3 install --upgrade pip +RUN apt update -y && apt install -y curl dumb-init python3 python3-venv python3-pip && rm -rf /var/lib/apt/lists/* +RUN python3 -m venv /python3env COPY requirements.txt / -RUN pip3 install --trusted-host pypi.python.org -r /requirements.txt +RUN /python3env/bin/pip3 install --trusted-host pypi.python.org -r /requirements.txt ENV APP_HOME /app -COPY app/* ${APP_HOME}/app/ -COPY server_info.json ${APP_HOME} - -COPY server.crt ${APP_HOME} -COPY server.key ${APP_HOME} +COPY app/* $APP_HOME/app/ +COPY build-info.json $APP_HOME +COPY server.crt $APP_HOME +COPY server.key $APP_HOME EXPOSE 8080 EXPOSE 8443 -WORKDIR ${APP_HOME} +WORKDIR $APP_HOME COPY docker-entrypoint.sh / -RUN chmod +x /docker-entrypoint.sh - ENTRYPOINT ["/usr/bin/dumb-init", "--"] CMD ["/docker-entrypoint.sh"] diff --git a/Makefile b/Makefile index 6e00fe6..6490ca7 100644 --- a/Makefile +++ b/Makefile @@ -1,31 +1,29 @@ -IMAGE_NAME:=jecklgamis/flask-app-example -IMAGE_TAG:=main +IMAGE_NAME:=flask-app-example +IMAGE_TAG:=$(shell git rev-parse --abbrev-ref HEAD) default: - echo $(IMAGE_TAG) - cat ./Makefile -dist: + @cat ./Makefile +install-deps: + @pip3 install -r requirements.txt +build: @./generate-ssl-certs.sh - @./generate-server-info.sh + @./generate-build-info.sh image: docker build -t $(IMAGE_NAME):$(IMAGE_TAG) . -run-shell: - @docker run -i -t $(IMAGE_NAME):$(IMAGE_TAG) /bin/bash run: - @docker run -p 8443:8443 -p 8080:8080 -it $(IMAGE_NAME):$(IMAGE_TAG) - -all : dist tests image + @docker run -p 8080:8080 -p 8443:8443 -it $(IMAGE_NAME):$(IMAGE_TAG) +run-shell: + @docker run -it $(IMAGE_NAME):$(IMAGE_TAG) /bin/bash +exec-shell: + docker exec -it `docker ps | grep $(IMAGE_NAME) | awk '{print $$1}'` /bin/bash +all: build check image up: all run - -install-deps: - @pip3 install -r requirements.txt - @pip3 install -r requirements-dev.txt -run-app-dev-mode: - @./run-app-dev-mode.sh -run-app-dev-mode-ssl: - @./run-app-dev-mode.sh ssl -smoke-tests: +run-smoke-tests: @./smoke-tests.py -.PHONY: tests -tests: - pytest -s +check: + @pytest -s +clean: + @find . -name build-info.json | xargs rm -f + @find . -name server.key | xargs rm -f + @find . -name server.crt | xargs rm -f + @find . -name *.log| xargs rm -f diff --git a/README.md b/README.md index 3bef927..bc506ff 100644 --- a/README.md +++ b/README.md @@ -2,35 +2,38 @@ [![Build](https://github.com/jecklgamis/flask-app-example/actions/workflows/build.yml/badge.svg)](https://github.com/jecklgamis/flask-app-example/actions/workflows/build.yml) -An example Flask app using Python 3 and Docker. +An example Flask app. Docker : `docker run -p 8080:8080 -it jecklgamis/flask-app-example:main` -What's in the box? +## What's In The Box? -* Ubuntu Linux based Docker image -* SSL/TLS listener -* Modular route handlers using [Flask Blueprints](https://flask.palletsprojects.com/en/1.1.x/blueprints/) +* Ubuntu Docker image * [Gunicorn](https://gunicorn.org) WSGI server -* Flask tests (under `tests` directory) using `pytest` and a `smoke-tests.py` for basic endpoint testing +* Build info, liveness and readiness probes +* PyTest unit tests +* HTTP/HTTPS listeners -## Preparing Your Environment -* Ensure [Python 3](https://www.python.org/downloads/), [Docker](https://www.docker.com/), and -[GNU Make](https://www.gnu.org/software/make/) are installed +## Requirements -Install Python dependencies: -``` -make install-deps -``` -## Building -To build the app: +* Python 3 +* Docker +* Make + +## Building + +Run `make install-deps` or `pip install -r requirements.txt` to install Python dependencies + +## Building + +Build Docker image ``` make all ``` This does a couple of things: * It generates self-signed SSL certificates (`server.key` and `server.crt`) -* It generates `server_info.json` that is served by the `/server_info` endpoint -* It runs tests using `pytest` +* It generates `build-info.json` that is served by the `/build-info` endpoint +* It runs tests * It generates a Docker image Explore the `Makefile` for details. @@ -42,9 +45,9 @@ make run ``` To run the app directly without using Docker: + ``` -make run-app-dev-mode -make run-app-dev-mode-ssl +flask run --host 0.0.0.0 --port 8080 ``` ## Testing The EndPoints @@ -53,6 +56,7 @@ make smoke-tests ``` ## Contributing -Please raise issue or pull request. Thanks for contributing! + +Please raise issue or pull request. Have fun! \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py index 5b65904..4d81707 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -18,16 +18,16 @@ def configure_logging(): def create_app(): configure_logging() app = Flask(__name__, instance_relative_config=True) - app.config.from_mapping( - SECRET_KEY=os.urandom(16) - ) + app.config.from_mapping(SECRET_KEY=os.urandom(16)) app.config.from_pyfile('config.py', silent=True) try: os.makedirs(app.instance_path) except OSError as e: pass - app.register_blueprint(api.bp) app.register_blueprint(root.bp) app.register_blueprint(probe.bp) return app + + +app = create_app() diff --git a/app/root.py b/app/root.py index 5b5c3ad..daf887f 100644 --- a/app/root.py +++ b/app/root.py @@ -12,7 +12,7 @@ def index(): return jsonify({"name": "flask-app-example", "message": "It works on my machine!"}) -@bp.route('/server_info', methods=['GET']) -def server_info(): - info = json.loads(io.open('server_info.json').read()) +@bp.route('/build-info', methods=['GET']) +def build_info(): + info = json.loads(io.open('build-info.json').read()) return jsonify(info) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index f59d361..c1b32a3 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,3 +1,8 @@ #!/usr/bin/env bash -# exec gunicorn --bind 0.0.0.0:8443 --certfile server.crt --keyfile server.key 'app:create_app()' -exec gunicorn --bind 0.0.0.0:8080 'app:create_app()' +source /python3env/bin/activate + +if [ "$USE_SSL" = "true" ]; then + exec gunicorn --bind 0.0.0.0:8443 --certfile server.crt --keyfile server.key 'app:app' +else + exec gunicorn --bind 0.0.0.0:8080 'app:app' +fi diff --git a/generate-server-info.sh b/generate-build-info.sh similarity index 64% rename from generate-server-info.sh rename to generate-build-info.sh index d641758..9836017 100755 --- a/generate-server-info.sh +++ b/generate-build-info.sh @@ -1,5 +1,5 @@ #!/bin/bash BRANCH=$(git rev-parse --abbrev-ref HEAD) COMMIT_ID=$(git rev-parse HEAD) -echo "{ \"version\":\"${COMMIT_ID}\", \"branch\":\"${BRANCH}\", \"name\":\"flask-app-example\"}" > server_info.json -echo "Wrote server_info.json" \ No newline at end of file +echo "{ \"version\":\"${COMMIT_ID}\", \"branch\":\"${BRANCH}\", \"name\":\"flask-app-example\"}" > build-info.json +echo "Wrote build-info.json" \ No newline at end of file diff --git a/generate-ssl-certs.sh b/generate-ssl-certs.sh index 09c861a..f5e345c 100755 --- a/generate-ssl-certs.sh +++ b/generate-ssl-certs.sh @@ -3,7 +3,7 @@ SERVER_KEY=server.key SERVER_CERT=server.crt rm -f ${SERVER_KEY} rm -f ${SERVER_CERT} -SUBJECT="/C=CC/ST=State/L=Locatlity/O=Org/OU=OrgUnit/CN=flask-app-example" > /dev/null +SUBJECT="/C=CC/ST=State/L=Locality/O=Org/OU=OrgUnit/CN=flask-app-example" > /dev/null openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ${SERVER_KEY} -out ${SERVER_CERT} -subj ${SUBJECT} > /dev/null openssl x509 -in ${SERVER_CERT} -text -noout > /dev/null echo "Wrote ${SERVER_KEY}" diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index a644583..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,2 +0,0 @@ -pytest==7.3.1 -flake8==6.0.0 diff --git a/requirements.txt b/requirements.txt index 67926c9..3cc90d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ -Flask==3.0.0 -gunicorn==21.2.0 -requests==2.31.0 +Flask==3.0.3 +gunicorn==22.0.0 +requests==2.32.3 +pytest==8.2.2 diff --git a/run-app-dev-mode.sh b/run-app.sh similarity index 60% rename from run-app-dev-mode.sh rename to run-app.sh index a085dbc..d01d742 100755 --- a/run-app-dev-mode.sh +++ b/run-app.sh @@ -3,12 +3,10 @@ USE_SSL=$1 export FLASK_APP=app -export FLASK_ENV=development +export FLASK_DEBUG=true -if [ ! -z "${USE_SSL}" ]; then - echo "Starting app using SSL" +if [ "$USE_SSL" = "true" ]; then flask run --cert server.crt --key server.key --host 0.0.0.0 --port 8443 else - echo "Starting app" flask run --host 0.0.0.0 --port 8080 fi diff --git a/smoke-tests.py b/smoke-tests.py index 479d810..c397b54 100755 --- a/smoke-tests.py +++ b/smoke-tests.py @@ -1,36 +1,16 @@ #!/usr/bin/env python3 -import logging as log -import sys -import unittest - import requests -import urllib3 - - -class SmokeTests(unittest.TestCase): - - def setUp(self) -> None: - SmokeTests.init() - - def testEndPointsExist(self): - log.info("Ensuring endpoints exist") - self.assertEqual(SmokeTests.get("https://localhost:8443/").status_code, 200) - self.assertEqual(SmokeTests.get("https://localhost:8443/server_info").status_code, 200) - self.assertEqual(SmokeTests.get("https://localhost:8443/api").status_code, 200) - @staticmethod - def get(url): - return requests.get(url, verify=False) - @staticmethod - def init(): - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - stdout_handler = log.StreamHandler(sys.stdout) - log.basicConfig(level=log.INFO, - format='[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s', - handlers=[stdout_handler]) +def test_endpoint_exists(): + assert requests.get("http://localhost:8080/").status_code == 200 + assert requests.get("http://localhost:8080/build-info").status_code == 200 if __name__ == '__main__': - unittest.main() + try: + test_endpoint_exists() + print("OK") + except: + print("NG") diff --git a/tests/test_root.py b/tests/test_root.py index 252e0e4..981d5b1 100644 --- a/tests/test_root.py +++ b/tests/test_root.py @@ -5,8 +5,8 @@ def test_index(client): assert "It works on my machine!" == data["message"] -def test_server_info(client): - response = client.get("/server_info") +def test_build_info(client): + response = client.get("/build-info") data = response.get_json() assert "flask-app-example" == data["name"] assert data["version"] is not None