commit 01ad74acd6a1d3b7b076789e0c5a35e530b3e1eb Author: Repinoid Date: Fri Oct 2 11:48:13 2026 +0300 Add MariaDB HTTP readiness service diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2e13771 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.git +.gitignore +.dockerignore +.venv +__pycache__ +*.pyc +.env +tests \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a7d3b48 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.venv/ +__pycache__/ +*.py[cod] +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8ad28d2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PORT=8080 + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt \ + && useradd --system --uid 10001 --create-home app + +COPY --chown=10001:10001 server.py . + +USER 10001:10001 +EXPOSE 8080 + +CMD ["python", "server.py"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..fa6dc09 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# MariaDB HTTP Check + +Small HTTP service for checking MariaDB connectivity from the Nubes HTTP runtime. + +Configuration is read from `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, and `DB_PASSWORD`. +The default listening port is `8080`; it can be changed with `PORT`. + +- `GET /healthz` checks that the process is serving requests. +- `GET /readyz` checks MariaDB with `SELECT 1` and returns `503` while the database is unavailable. +- `GET /` returns the same readiness status as `/readyz`. + +Build the container image with: + +```sh +docker build -t naeel/mariadb-check:0.1 . +``` \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..40d1802 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +PyMySQL>=1.1,<2 \ No newline at end of file diff --git a/server.py b/server.py new file mode 100644 index 0000000..72ea776 --- /dev/null +++ b/server.py @@ -0,0 +1,79 @@ +import json +import os +from http import HTTPStatus +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from urllib.parse import urlsplit + +import pymysql + + +PORT = int(os.environ.get("PORT", "8080")) +DB_CONFIG = { + "host": os.environ.get("DB_HOST", "127.0.0.1"), + "port": int(os.environ.get("DB_PORT", "3306")), + "database": os.environ.get("DB_NAME", "maria"), + "user": os.environ.get("DB_USER", ""), + "password": os.environ.get("DB_PASSWORD", ""), + "charset": "utf8mb4", + "connect_timeout": 3, + "read_timeout": 3, + "write_timeout": 3, + "autocommit": True, +} + + +def database_ready(): + connection = None + cursor = None + try: + connection = pymysql.connect(**DB_CONFIG) + cursor = connection.cursor() + cursor.execute("SELECT 1") + return cursor.fetchone() == (1,) + except (pymysql.MySQLError, OSError): + return False + finally: + if cursor is not None: + cursor.close() + if connection is not None: + connection.close() + + +class Handler(BaseHTTPRequestHandler): + server_version = "MariaDBCheck/1.0" + sys_version = "" + + def send_json(self, status, payload): + body = json.dumps(payload, separators=(",", ":")).encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.send_header("Cache-Control", "no-store") + self.end_headers() + self.wfile.write(body) + + def do_GET(self): + path = urlsplit(self.path).path + if path == "/healthz": + self.send_json(HTTPStatus.OK, {"status": "ok"}) + return + + if path in ("/", "/readyz"): + ready = database_ready() + status = HTTPStatus.OK if ready else HTTPStatus.SERVICE_UNAVAILABLE + self.send_json( + status, + { + "service": "maria-http", + "status": "ready" if ready else "unavailable", + "database": "connected" if ready else "unavailable", + }, + ) + return + + self.send_json(HTTPStatus.NOT_FOUND, {"error": "not_found"}) + + +if __name__ == "__main__": + server = ThreadingHTTPServer(("0.0.0.0", PORT), Handler) + server.serve_forever() \ No newline at end of file diff --git a/tests/test_server.py b/tests/test_server.py new file mode 100644 index 0000000..6d001f4 --- /dev/null +++ b/tests/test_server.py @@ -0,0 +1,29 @@ +import unittest +from unittest.mock import MagicMock, patch + +import pymysql + +import server + + +class DatabaseReadyTests(unittest.TestCase): + @patch("server.pymysql.connect") + def test_returns_true_for_select_one(self, connect): + connection = MagicMock() + cursor = connection.cursor.return_value + cursor.fetchone.return_value = (1,) + connect.return_value = connection + + self.assertTrue(server.database_ready()) + cursor.execute.assert_called_once_with("SELECT 1") + cursor.close.assert_called_once() + connection.close.assert_called_once() + + @patch("server.pymysql.connect", side_effect=pymysql.MySQLError("unavailable")) + def test_returns_false_when_database_is_unavailable(self, connect): + self.assertFalse(server.database_ready()) + connect.assert_called_once() + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file