Add MariaDB HTTP readiness service
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
.git
|
||||
.gitignore
|
||||
.dockerignore
|
||||
.venv
|
||||
__pycache__
|
||||
*.pyc
|
||||
.env
|
||||
tests
|
||||
@@ -0,0 +1,4 @@
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.env
|
||||
+18
@@ -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"]
|
||||
@@ -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 .
|
||||
```
|
||||
@@ -0,0 +1 @@
|
||||
PyMySQL>=1.1,<2
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user