diff --git a/.github/workflows/develop.yaml b/.github/workflows/develop.yaml index 8075f22..37fe368 100644 --- a/.github/workflows/develop.yaml +++ b/.github/workflows/develop.yaml @@ -17,7 +17,7 @@ env: jobs: check_for_linux: - name: "Run checks and tests on Linux" + name: Run checks and tests on Linux runs-on: ubuntu-latest steps: @@ -48,7 +48,7 @@ jobs: check_for_windows: - name: "Run checks and tests on Windows" + name: Run checks and tests on Windows runs-on: windows-latest steps: @@ -75,3 +75,18 @@ jobs: - name: Run tests run: .venv-automation/Scripts/automation.exe --verbosity debug test + + + + build_docker_image: + name: Build docker image + runs-on: ubuntu-latest + needs: check_for_linux + + steps: + + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Build docker image + run: docker build --file Deployment/dockerfile . diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index b0c7d34..e473389 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -17,7 +17,7 @@ env: jobs: check_for_linux: - name: "Run checks and tests on Linux" + name: Run checks and tests on Linux runs-on: ubuntu-latest steps: @@ -48,7 +48,7 @@ jobs: check_for_windows: - name: "Run checks and tests on Windows" + name: Run checks and tests on Windows runs-on: windows-latest steps: @@ -75,3 +75,18 @@ jobs: - name: Run tests run: .venv-automation/Scripts/automation.exe --verbosity debug test + + + + build_docker_image: + name: Build docker image + runs-on: ubuntu-latest + needs: check_for_linux + + steps: + + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Build docker image + run: docker build --file Deployment/dockerfile . diff --git a/Deployment/application.json b/Deployment/application.json new file mode 100644 index 0000000..8741598 --- /dev/null +++ b/Deployment/application.json @@ -0,0 +1,4 @@ +{ + "flask_secret_key": "secret", + "metrics_token": "metrics" +} diff --git a/Deployment/compose.yaml b/Deployment/compose.yaml new file mode 100644 index 0000000..bbd18a3 --- /dev/null +++ b/Deployment/compose.yaml @@ -0,0 +1,23 @@ +# cspell:words gunicorn + +name: developer-website + +services: + developer_website: + build: + context: .. + dockerfile: Deployment/dockerfile + environment: + - APPLICATION_CONFIGURATION=/srv/application.json + ports: [ "8080:80" ] + configs: + - source: application + target: /srv/application.json + - source: gunicorn + target: /srv/gunicorn_configuration.py + +configs: + application: + file: application.json + gunicorn: + file: gunicorn_configuration.py diff --git a/Deployment/dockerfile b/Deployment/dockerfile new file mode 100644 index 0000000..809f312 --- /dev/null +++ b/Deployment/dockerfile @@ -0,0 +1,27 @@ +# cspell:words a2enmod gunicorn libapache2 venv virtualenv wsgi + +FROM debian:12 + + + +# Install Python +RUN apt update && apt install --yes python3 python3-pip python3-venv + +# Copy the application sources +COPY Sources /srv/application + +# Set up the python virtual environment +RUN python3 -m venv /srv/application/.venv +RUN /srv/application/.venv/bin/python -m pip install --upgrade pip wheel +COPY pip.conf /srv/application/.venv/pip.conf + +# Install the application and gunicorn +RUN /srv/application/.venv/bin/python -m pip install --upgrade /srv/application/website "gunicorn ~= 23.0.0" + + + +# Open network port +EXPOSE 80 + +# Run gunicorn +CMD [ "/srv/application/.venv/bin/gunicorn", "--bind", "0.0.0.0:80", "--config", "/srv/gunicorn_configuration.py" ] diff --git a/Deployment/gunicorn_configuration.py b/Deployment/gunicorn_configuration.py new file mode 100644 index 0000000..fdb330a --- /dev/null +++ b/Deployment/gunicorn_configuration.py @@ -0,0 +1,43 @@ +# cspell:words levelname + +import multiprocessing + +from benjaminhamon_standard_extensions.logging import logging_helpers + + +wsgi_app = "benjaminhamon_developer_website.wsgi_entry_point" + +workers = multiprocessing.cpu_count() * 2 + 1 + +logconfig_dict = { + "version": 1, + + "root": { + "level": "DEBUG", + "handlers": [ "stdout" ], + }, + + "loggers": { + "gunicorn.error": { + "level": "INFO", + "propagate": True, + }, + }, + + "handlers": { + "stdout": { + "class": "logging.StreamHandler", + "formatter": "generic", + "stream": "ext://sys.stdout", + }, + }, + + "formatters": { + "generic": { + "style": "{", + "format": "{asctime} [{levelname}][{name}] ({process}) {message}", + "datefmt": logging_helpers.date_format_iso, + "class": "logging.Formatter", + } + } +} diff --git a/Sources/website/benjaminhamon_developer_website/run.py b/Sources/website/benjaminhamon_developer_website/run.py index 7f8561d..e6debe0 100644 --- a/Sources/website/benjaminhamon_developer_website/run.py +++ b/Sources/website/benjaminhamon_developer_website/run.py @@ -19,7 +19,6 @@ def main(): arguments = argument_parser.parse_args() configure_logging(arguments) - logging.getLogger("werkzeug").setLevel(logging.WARNING) application = application_factory.create_application("secret", "metrics") website_url = "http://%s:%s/" % (arguments.address, arguments.port) @@ -36,8 +35,6 @@ def create_argument_parser() -> argparse.ArgumentParser: help = "set the address for the server to listen to") argument_parser.add_argument("--port", required = True, type = int, help = "set the port for the server to listen to") - argument_parser.add_argument("--secret", required = True, - help = "set the flask application secret key") argument_parser.add_argument("--verbosity", choices = logging_helpers.all_log_levels, metavar = "", help = "set the logging level (%s)" % ", ".join(logging_helpers.all_log_levels)) @@ -76,6 +73,8 @@ def configure_logging(arguments: argparse.Namespace): if log_file_path is not None: logging_helpers.configure_log_file(logging.root, log_file_path, log_file_verbosity, message_format, date_format, mode = "w", encoding = "utf-8") + logging.getLogger("werkzeug").setLevel(logging.WARNING) + if __name__ == "__main__": try: diff --git a/Sources/website/benjaminhamon_developer_website/wsgi_entry_point.py b/Sources/website/benjaminhamon_developer_website/wsgi_entry_point.py new file mode 100644 index 0000000..acf9dc1 --- /dev/null +++ b/Sources/website/benjaminhamon_developer_website/wsgi_entry_point.py @@ -0,0 +1,19 @@ +import json +import logging +import os + +import benjaminhamon_developer_website +from benjaminhamon_developer_website import application_factory + + +logger = logging.getLogger("Main") + +logger.info("Instancing application for WSGI (version: %s)", benjaminhamon_developer_website.__version__) + +configuration_file_path = os.environ["APPLICATION_CONFIGURATION"] +with open(configuration_file_path, mode = "r", encoding = "utf-8") as configuration_file: + configuration = json.load(configuration_file) + +application = application_factory.create_application( + flask_secret_key = configuration["flask_secret_key"], + metrics_token = configuration["metrics_token"]) diff --git a/Tests/website/benjaminhamon_developer_website_tests/test_application.py b/Tests/website/benjaminhamon_developer_website_tests/test_application.py index 8eb497d..0c15b05 100644 --- a/Tests/website/benjaminhamon_developer_website_tests/test_application.py +++ b/Tests/website/benjaminhamon_developer_website_tests/test_application.py @@ -14,7 +14,7 @@ async def website_fixture(): address = "localhost" port = 4999 - command = [ python_executable, "-m", application_module, "--address", address, "--port", str(port), "--secret", "secret" ] + command = [ python_executable, "-m", application_module, "--address", address, "--port", str(port) ] async with WebsiteRunner(command, address, port) as website: yield website.get_url()