From 51f5d102fbcb69357ab754e27cf62956f73ffc03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Oliveira?= Date: Fri, 19 Jul 2024 10:20:47 +0100 Subject: [PATCH] Configuration added to wait for database. --- .github/workflows/checks.yml | 2 +- Dockerfile | 4 ++ app/app/calc.py | 13 ++++++ app/app/settings.py | 9 +++- app/app/test.py | 22 ++++++++++ app/core/__init__.py | 0 app/core/admin.py | 3 ++ app/core/apps.py | 6 +++ app/core/management/__init__.py | 0 app/core/management/commands/__init__.py | 0 app/core/management/commands/wait_for_db.py | 24 +++++++++++ app/core/migrations/__init__.py | 0 app/core/models.py | 3 ++ app/core/tests/__init__.py | 0 app/core/tests/test_commands.py | 46 +++++++++++++++++++++ docker-compose.yml | 24 ++++++++++- requirements.txt | 1 + 17 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 app/app/calc.py create mode 100644 app/app/test.py create mode 100644 app/core/__init__.py create mode 100644 app/core/admin.py create mode 100644 app/core/apps.py create mode 100644 app/core/management/__init__.py create mode 100644 app/core/management/commands/__init__.py create mode 100644 app/core/management/commands/wait_for_db.py create mode 100644 app/core/migrations/__init__.py create mode 100644 app/core/models.py create mode 100644 app/core/tests/__init__.py create mode 100644 app/core/tests/test_commands.py diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 714c219..dc9ff0a 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -16,6 +16,6 @@ jobs: - name: Checkout uses: actions/checkout@v2 - name: Test - run: docker-compose run --rm app sh -c "python manage.py test" + run: docker-compose run --rm app sh -c "python manage.py wait_for_db && python manage.py test" - name: Lint run: docker-compose run --rm app sh -c "flake8" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 92d60af..7b0eba8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,11 +12,15 @@ EXPOSE 8000 ARG DEV=false RUN python -m venv /py && \ /py/bin/pip install --upgrade pip && \ + apk add --update --no-cache postgresql-client && \ + apk add --update --no-cache --virtual .tmp-build-deps \ + build-base postgresql-dev musl-dev && \ /py/bin/pip install -r /tmp/requirements.txt && \ if [ $DEV = "true" ]; \ then /py/bin/pip install -r /tmp/requirements.dev.txt ; \ fi && \ rm -rf /tmp && \ + apk del .tmp-build-deps && \ adduser \ --disabled-password \ --no-create-home \ diff --git a/app/app/calc.py b/app/app/calc.py new file mode 100644 index 0000000..e6cb965 --- /dev/null +++ b/app/app/calc.py @@ -0,0 +1,13 @@ +""" +Calculator functions +""" + + +def add(x, y): + """Add x and y and return result.""" + return x + y + + +def subtract(x, y): + """ Subtract x from y and return result.""" + return y - x diff --git a/app/app/settings.py b/app/app/settings.py index d770813..475c985 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -10,6 +10,7 @@ https://docs.djangoproject.com/en/3.2/ref/settings/ """ +import os from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -37,6 +38,7 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'core', ] MIDDLEWARE = [ @@ -75,8 +77,11 @@ DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + 'ENGINE': 'django.db.backends.postgresql', + 'HOST': os.environ.get('DB_HOST'), + 'NAME': os.environ.get('DB_NAME'), + 'USER': os.environ.get('DB_USER'), + 'PASSWORD': os.environ.get('DB_PASS'), } } diff --git a/app/app/test.py b/app/app/test.py new file mode 100644 index 0000000..336fffa --- /dev/null +++ b/app/app/test.py @@ -0,0 +1,22 @@ +""" +Sample tests +""" +from django.test import SimpleTestCase + +from app import calc + + +class CalcTests(SimpleTestCase): + """Test the calc module""" + + def test_add_numbers(self): + """Test adding numbers together.""" + res = calc.add(5, 6) + + self.assertEqual(res, 11) + + def test_subtract_numbers(self): + """Test subtracting numbers.""" + res = calc.subtract(10, 15) + + self.assertEqual(res, 5) diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/admin.py b/app/core/admin.py new file mode 100644 index 0000000..a4e11e9 --- /dev/null +++ b/app/core/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin # noqa + +# Register your models here. diff --git a/app/core/apps.py b/app/core/apps.py new file mode 100644 index 0000000..8115ae6 --- /dev/null +++ b/app/core/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'core' diff --git a/app/core/management/__init__.py b/app/core/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/management/commands/__init__.py b/app/core/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/management/commands/wait_for_db.py b/app/core/management/commands/wait_for_db.py new file mode 100644 index 0000000..0d716b5 --- /dev/null +++ b/app/core/management/commands/wait_for_db.py @@ -0,0 +1,24 @@ +""" +Django command to wait for the database to be available. +""" + +import time +from psycopg2 import OperationalError as Psycopg2OpError + +from django.db.utils import OperationalError +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + """Base command to wait for the database.""" + def handle(self, *args, **options): + self.stdout.write('Waiting for database...') + db_up = False + while db_up is False: + try: + self.check(databases=['default']) + db_up = True + except (Psycopg2OpError, OperationalError): + self.stdout.write('Database unavailable, waiting 1 second...') + time.sleep(1) + self.stdout.write(self.style.SUCCESS('Database available!')) diff --git a/app/core/migrations/__init__.py b/app/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/models.py b/app/core/models.py new file mode 100644 index 0000000..56b48a5 --- /dev/null +++ b/app/core/models.py @@ -0,0 +1,3 @@ +from django.db import models # noqa + +# Create your models here. diff --git a/app/core/tests/__init__.py b/app/core/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/tests/test_commands.py b/app/core/tests/test_commands.py new file mode 100644 index 0000000..73073a6 --- /dev/null +++ b/app/core/tests/test_commands.py @@ -0,0 +1,46 @@ +""" +Test Django management commands. +""" + +from unittest.mock import patch + +from psycopg2 import OperationalError as Psycopg2Error + +from django.core.management import call_command +from django.db.utils import OperationalError +from django.test import SimpleTestCase + + +@patch('core.management.commands.wait_for_db.Command.check') +class CommandTests(SimpleTestCase): + """Test commands.""" + + def test_wait_for_db_ready(self, patched_check): + """Test waiting for database if database ready.""" + patched_check.return_value = True + + call_command('wait_for_db') + + patched_check.assert_called_once_with(databases=['default']) + + @patch('time.sleep') + def test_wait_for_db_delay(self, patched_sleep, patched_check): + """Test waiting for database when getting OperationalError.""" + + """ + This is the way we throw exceptions instead of returning a + simple value (True of False). Recall that patch is simulating the + behaviour of the connection to the database. + """ + patched_check.side_effect = [Psycopg2Error] * 2 + \ + [OperationalError] * 3 + [True] + + call_command('wait_for_db') + + """ + We expect to call the method 6 times until we get a True. + That's defined in patched_check list. + """ + self.assertEqual(patched_check.call_count, 6) + + patched_check.assert_called_with(databases=['default']) diff --git a/docker-compose.yml b/docker-compose.yml index 2cd1bb9..3e8296b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,5 +11,27 @@ services: volumes: - ./app:/app command: > - sh -c "python manage.py runserver 0.0.0.0:8000" + sh -c "python manage.py wait_for_db && + python manage.py migrate && + python manage.py runserver 0.0.0.0:8000" + environment: + - DB_HOST=db + - DB_NAME=devdb + - DB_USER=devuser + - DB_PASS=changeme + depends_on: + - db + db: + image: postgres:13-alpine + volumes: + - dev-db-data:/var/lib/postgresql/data + environment: + - POSTGRES_DB=devdb + - POSTGRES_USER=devuser + - POSTGRES_PASSWORD=changeme + + + +volumes: + dev-db-data: diff --git a/requirements.txt b/requirements.txt index 4c9c02a..8d29a59 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ Django>=3.2.4,<3.3 djangorestframework>=3.12.4,<3.13 +psycopg2>=2.8.6,<2.9 \ No newline at end of file