Skip to content

Commit 5967685

Browse files
committed
test(mysql-connections): pilot a testcontainers integration test
mysql-connections opens a real pymysql connection and runs SHOW GLOBAL STATUS / SHOW GLOBAL VARIABLES against the server, so it cannot be tested with static fixtures - we have to spin up a real MariaDB. testcontainers-python (via lib.lftest.run_container) handles the container lifecycle and port mapping, then the plugin is invoked from the host with a --defaults-file pointing at a tiny temporary .my.cnf that carries the container's host, port and credentials. Container source: quay.io/sclorg/mariadb-1011-c10s, the Red Hat / CentOS Stream 10 sclorg packaging of MariaDB 10.11 LTS. This is the flavour that customers actually run in production, not just an upstream tarball. Two non-obvious things the test has to work around: - The sclorg run-mysqld entrypoint defaults to a unix-socket-only binding (`port: 0` in the version banner), so remote pymysql connections fail with "Lost connection during query". Forcing `run-mysqld --port=3306` brings the TCP listener up. - The wait_log marker has to anchor on the FINAL "port: 3306" line in the version banner, not on the earlier "ready for connections" string that mysqld emits before the TCP listener is bound. This file is the pilot for the wider mysql-* container-test family. Per CONTRIBUTING's "Combine container tests with fixtures" rule the file only carries the happy-path integration scenario; the interesting edge cases for mysql-connections (runaway connection pool, exhausted max_connections, name resolution turned on) belong in a separate fixture-based pass that mocks the SHOW STATUS output.
1 parent 7573fc8 commit 5967685

File tree

1 file changed

+146
-0
lines changed
  • check-plugins/mysql-connections/unit-test

1 file changed

+146
-0
lines changed
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8; py-indent-offset: 4 -*-
3+
#
4+
# Author: Linuxfabrik GmbH, Zurich, Switzerland
5+
# Contact: info (at) linuxfabrik (dot) ch
6+
# https://www.linuxfabrik.ch/
7+
# License: The Unlicense, see LICENSE file.
8+
9+
# https://github.com/Linuxfabrik/monitoring-plugins/blob/main/CONTRIBUTING.md
10+
11+
"""Integration tests for the mysql-connections check plugin.
12+
13+
This is the pilot for the mysql-* container-test family. The plugin
14+
opens a real `pymysql` connection and runs `SHOW GLOBAL STATUS` and
15+
`SHOW GLOBAL VARIABLES` queries, so it cannot be exercised with a
16+
static fixture - we have to spin up an actual MariaDB / MySQL server
17+
and let the plugin talk to it over TCP. testcontainers-python (via
18+
`lib.lftest.run_container()`) handles the container lifecycle and
19+
port mapping, then the plugin is invoked from the host with a
20+
`--defaults-file` pointing at a tiny temporary `.my.cnf` containing
21+
the container's host, port and credentials.
22+
23+
Container source: `quay.io/sclorg/mariadb-1011-c10s` - the Red Hat /
24+
CentOS Stream 10 sclorg packaging of MariaDB 10.11 LTS. This is a
25+
real customer-ish environment, not just an upstream binary.
26+
27+
Per CONTRIBUTING's "Combine container tests with fixtures" rule,
28+
this file holds only the happy-path integration test (a freshly
29+
booted, lightly-loaded MariaDB always reports a few percent of used
30+
connections, which is STATE_OK with the plugin's defaults). The
31+
interesting edge cases for mysql-connections (a runaway connection
32+
pool, exhausted max_connections, name resolution turned on by
33+
mistake, SQL errors) belong in a fixture-based pass that mocks the
34+
SHOW STATUS output - that is a separate follow-up.
35+
36+
Requirements:
37+
- podman (or docker) with a reachable socket
38+
- `pip install testcontainers`
39+
- on rootless podman, `tools/run-unit-tests` auto-sets
40+
`CONTAINER_HOST` (the user-facing variable) and
41+
`TESTCONTAINERS_RYUK_DISABLED`. Internally the helper still
42+
populates `DOCKER_HOST` from `CONTAINER_HOST` because
43+
testcontainers-python only knows about the docker variable name.
44+
"""
45+
46+
import os
47+
import subprocess
48+
import sys
49+
import tempfile
50+
import unittest
51+
52+
sys.path.insert(0, '..')
53+
54+
import lib.lftest
55+
from lib.globals import STATE_OK
56+
57+
58+
# Single Red Hat family MariaDB image for the pilot. After the pilot
59+
# is wired into the rest of the mysql-* family we can grow this list
60+
# to cover MariaDB 10.6 / 10.11 / 11.4 LTS plus MySQL 8.0 / 8.4.
61+
IMAGES = [
62+
('quay.io/sclorg/mariadb-1011-c10s', 'MariaDB 10.11'),
63+
]
64+
65+
66+
class TestCheck(unittest.TestCase):
67+
def test(self):
68+
for image, label in IMAGES:
69+
with self.subTest(image=image):
70+
print(f'\n=== Testing {label} ({image}) ===', flush=True)
71+
with lib.lftest.run_container(
72+
image,
73+
env={
74+
'MYSQL_ROOT_PASSWORD': 'test',
75+
'MYSQL_USER': 'test',
76+
'MYSQL_PASSWORD': 'test',
77+
'MYSQL_DATABASE': 'test',
78+
},
79+
ports=[3306],
80+
# The sclorg image's run-mysqld entrypoint
81+
# disables the TCP listener by default and only
82+
# binds the unix socket, so the version banner
83+
# ends with `port: 0` and remote connections fail.
84+
# Force `--port=3306` to bring the TCP listener up.
85+
command='run-mysqld --port=3306',
86+
# Wait for the "port: 3306" segment of the version
87+
# banner that mariadbd writes once the TCP listener
88+
# is actually bound.
89+
wait_log='port: 3306',
90+
wait_log_timeout=180,
91+
) as container:
92+
host = container.get_container_host_ip()
93+
port = container.get_exposed_port(3306)
94+
95+
with tempfile.NamedTemporaryFile(
96+
mode='w',
97+
suffix='.cnf',
98+
delete=False,
99+
) as cnf:
100+
cnf.write(
101+
f'[client]\n'
102+
f'host={host}\n'
103+
f'port={port}\n'
104+
f'user=test\n'
105+
f'password=test\n'
106+
f'database=test\n'
107+
)
108+
defaults_file = cnf.name
109+
110+
try:
111+
cmd = [
112+
'python3', '../mysql-connections',
113+
f'--defaults-file={defaults_file}',
114+
]
115+
print(f'Run plugin: {" ".join(cmd)}', flush=True)
116+
result = subprocess.run(
117+
cmd,
118+
cwd=os.path.dirname(os.path.abspath(__file__)),
119+
capture_output=True,
120+
text=True,
121+
)
122+
finally:
123+
os.unlink(defaults_file)
124+
125+
combined = result.stdout + result.stderr
126+
print(f'Script output:\n{combined.strip()}', flush=True)
127+
128+
# A freshly booted, lightly-loaded server is
129+
# always STATE_OK with the plugin's defaults.
130+
self.assertEqual(result.returncode, STATE_OK)
131+
self.assertRegex(
132+
combined,
133+
r'\d+(\.\d+)?% aborted connections',
134+
)
135+
self.assertRegex(
136+
combined,
137+
r'current \d+(\.\d+)?% used',
138+
)
139+
self.assertRegex(
140+
combined,
141+
r'peak \d+(\.\d+)?% used',
142+
)
143+
144+
145+
if __name__ == '__main__':
146+
unittest.main()

0 commit comments

Comments
 (0)