Skip to content

Commit 716da08

Browse files
Add TLS integration test + test COS when related to TLS operator
1 parent 6dae91c commit 716da08

File tree

5 files changed

+205
-90
lines changed

5 files changed

+205
-90
lines changed

poetry.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ requests = "^2.31.0"
2020
# data_platform_libs/v0/data_interfaces.py
2121
ops = ">=2.0.0"
2222
# tls_certificates_interface/v1/tls_certificates.py
23-
cryptography = "*"
23+
cryptography = ">=42.0.5"
2424
jsonschema = "*"
2525
# grafana_agent/v0/cos_agent.py
2626
pydantic = "<2"

tests/integration/helpers.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import itertools
55
import tempfile
6-
from typing import Dict, List
6+
from typing import Dict, List, Optional
77

88
from juju.unit import Unit
99
from pytest_operator.plugin import OpsTest
@@ -222,3 +222,21 @@ async def stop_running_flush_mysqlrouter_cronjobs(ops_test: OpsTest, unit_name:
222222
with attempt:
223223
if await get_process_pid(ops_test, unit_name, "logrotate"):
224224
raise Exception("Failed to stop the flush_mysql_logs logrotate process")
225+
226+
227+
async def get_tls_certificate_issuer(
228+
ops_test: OpsTest,
229+
unit_name: str,
230+
socket: Optional[str] = None,
231+
host: Optional[str] = None,
232+
port: Optional[int] = None,
233+
) -> str:
234+
connect_args = f"-unix {socket}" if socket else f"-connect {host}:{port}"
235+
get_tls_certificate_issuer_commands = [
236+
"ssh",
237+
unit_name,
238+
f"openssl s_client -showcerts -starttls mysql {connect_args} < /dev/null | openssl x509 -text | grep Issuer",
239+
]
240+
return_code, issuer, _ = await ops_test.juju(*get_tls_certificate_issuer_commands)
241+
assert return_code == 0, f"failed to get TLS certificate issuer on {unit_name=}"
242+
return issuer

tests/integration/test_exporter.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
MYSQL_ROUTER_APP_NAME = "mysql-router"
1717
APPLICATION_APP_NAME = "mysql-test-app"
1818
GRAFANA_AGENT_APP_NAME = "grafana-agent"
19+
TLS_APP_NAME = "tls-certificates-operator"
1920
SLOW_TIMEOUT = 25 * 60
2021

2122

@@ -149,3 +150,76 @@ async def test_exporter_endpoint(ops_test: OpsTest, mysql_router_charm_series: s
149150
), "❌ expected connection refused error"
150151
else:
151152
assert False, "❌ can connect to metrics endpoint without relation with cos"
153+
154+
155+
@pytest.mark.group(1)
156+
@pytest.mark.abort_on_fail
157+
async def test_exporter_endpoint_with_tls(ops_test: OpsTest) -> None:
158+
"""Test that the exporter endpoint works when related with TLS"""
159+
http = urllib3.PoolManager()
160+
161+
mysql_router_app = ops_test.model.applications[MYSQL_ROUTER_APP_NAME]
162+
163+
logger.info(f"Deploying {TLS_APP_NAME}")
164+
await ops_test.model.deploy(
165+
TLS_APP_NAME,
166+
application_name=TLS_APP_NAME,
167+
channel="stable",
168+
config={"generate-self-signed-certificates": "true", "ca-common-name": "Test CA"},
169+
)
170+
171+
await ops_test.model.wait_for_idle([TLS_APP_NAME], status="active", timeout=SLOW_TIMEOUT)
172+
173+
logger.info(f"Relation mysqlrouter with {TLS_APP_NAME}")
174+
175+
await ops_test.model.relate(
176+
f"{MYSQL_ROUTER_APP_NAME}:certificates", f"{TLS_APP_NAME}:certificates"
177+
)
178+
179+
time.sleep(30)
180+
181+
mysql_test_app = ops_test.model.applications[APPLICATION_APP_NAME]
182+
unit_address = await mysql_test_app.units[0].get_public_address()
183+
184+
try:
185+
http.request("GET", f"http://{unit_address}:49152/metrics")
186+
except urllib3.exceptions.MaxRetryError as e:
187+
assert (
188+
"[Errno 111] Connection refused" in e.reason.args[0]
189+
), "❌ expected connection refused error"
190+
else:
191+
assert False, "❌ can connect to metrics endpoint without relation with cos"
192+
193+
logger.info("Relating mysqlrouter with grafana agent")
194+
await ops_test.model.relate(
195+
f"{GRAFANA_AGENT_APP_NAME}:cos-agent", f"{MYSQL_ROUTER_APP_NAME}:cos-agent"
196+
)
197+
198+
time.sleep(30)
199+
200+
jmx_resp = http.request("GET", f"http://{unit_address}:49152/metrics")
201+
assert jmx_resp.status == 200, "❌ cannot connect to metrics endpoint with relation with cos"
202+
assert "mysqlrouter_route_health" in str(
203+
jmx_resp.data
204+
), "❌ did not find expected metric in response"
205+
206+
logger.info("Removing relation between mysqlrouter and grafana agent")
207+
await mysql_router_app.remove_relation(
208+
f"{GRAFANA_AGENT_APP_NAME}:cos-agent", f"{MYSQL_ROUTER_APP_NAME}:cos-agent"
209+
)
210+
211+
time.sleep(30)
212+
213+
try:
214+
http.request("GET", f"http://{unit_address}:49152/metrics")
215+
except urllib3.exceptions.MaxRetryError as e:
216+
assert (
217+
"[Errno 111] Connection refused" in e.reason.args[0]
218+
), "❌ expected connection refused error"
219+
else:
220+
assert False, "❌ can connect to metrics endpoint without relation with cos"
221+
222+
logger.info(f"Removing relation between mysqlrouter and {TLS_APP_NAME}")
223+
await mysql_router_app.remove_relation(
224+
f"{MYSQL_ROUTER_APP_NAME}:certificates", f"{TLS_APP_NAME}:certificates"
225+
)

tests/integration/test_tls.py

Lines changed: 110 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,112 @@
1-
# Copyright 2023 Canonical Ltd.
1+
# Copyright 2024 Canonical Ltd.
22
# See LICENSE file for licensing details.
33

4-
# flake8: noqa
5-
# TODO: enable & remove noqa
6-
# import asyncio
7-
# import logging
8-
#
9-
# import pytest
10-
# from pytest_operator.plugin import OpsTest
11-
#
12-
# logger = logging.getLogger(__name__)
13-
#
14-
# MYSQL_APP_NAME = "mysql"
15-
# MYSQL_ROUTER_APP_NAME = "mysqlrouter"
16-
# TEST_APP_NAME = "mysql-test-app"
17-
# TLS_APP_NAME = "tls-certificates-operator"
18-
# SLOW_TIMEOUT = 15 * 60
19-
# MODEL_CONFIG = {"logging-config": "<root>=INFO;unit=DEBUG"}
20-
#
21-
#
22-
# @pytest.mark.group(1)
23-
# @pytest.mark.abort_on_fail
24-
# async def test_build_deploy_and_relate(ops_test: OpsTest, mysql_router_charm_series: str) -> None:
25-
# """Test encryption when backend database is using TLS."""
26-
# # Deploy TLS Certificates operator.
27-
# await ops_test.model.set_config(MODEL_CONFIG)
28-
# logger.info("Deploy and relate all applications")
29-
# async with ops_test.fast_forward():
30-
# # deploy mysql first
31-
# await ops_test.model.deploy(
32-
# MYSQL_APP_NAME, channel="8.0/edge", config={"profile": "testing"}, num_units=3
33-
# )
34-
# tls_config = {"generate-self-signed-certificates": "true", "ca-common-name": "Test CA"}
35-
#
36-
# # ROUTER
37-
# mysqlrouter_charm = await ops_test.build_charm(".")
38-
#
39-
# # tls, test app and router
40-
# await asyncio.gather(
41-
# ops_test.model.deploy(
42-
# mysqlrouter_charm,
43-
# application_name=MYSQL_ROUTER_APP_NAME,
44-
# num_units=None,
45-
# series=mysql_router_charm_series,
46-
# ),
47-
# ops_test.model.deploy(
48-
# TLS_APP_NAME, application_name=TLS_APP_NAME, channel="stable", config=tls_config
49-
# ),
50-
# ops_test.model.deploy(
51-
# TEST_APP_NAME, application_name=TEST_APP_NAME, channel="latest/edge"
52-
# ),
53-
# )
54-
#
55-
# await ops_test.model.relate(
56-
# f"{MYSQL_ROUTER_APP_NAME}:backend-database", f"{MYSQL_APP_NAME}:database"
57-
# )
58-
# await ops_test.model.relate(
59-
# f"{TEST_APP_NAME}:database", f"{MYSQL_ROUTER_APP_NAME}:database"
60-
# )
61-
#
62-
# logger.info("Waiting for applications to become active")
63-
# # We can safely wait only for test application to be ready, given that it will
64-
# # only become active once all the other applications are ready.
65-
# await ops_test.model.wait_for_idle([TEST_APP_NAME], status="active", timeout=15 * 60)
66-
#
67-
#
68-
# @pytest.mark.group(1)
69-
# async def test_connected_encryption(ops_test: OpsTest) -> None:
70-
# """Test encryption when backend database is using TLS."""
71-
# test_app_unit = ops_test.model.applications[TEST_APP_NAME].units[0]
72-
#
73-
# logger.info("Relating TLS with backend database")
74-
# await ops_test.model.relate(TLS_APP_NAME, MYSQL_APP_NAME)
75-
#
76-
# # Wait for hooks start reconfiguring app
77-
# await ops_test.model.block_until(
78-
# lambda: ops_test.model.applications[MYSQL_APP_NAME].status != "active", timeout=4 * 60
79-
# )
80-
# await ops_test.model.wait_for_idle(status="active", timeout=15 * 60)
81-
#
82-
# logger.info("Get cipher when TLS is enforced")
83-
# action = await test_app_unit.run_action("get-session-ssl-cipher")
84-
# result = await action.wait()
85-
#
86-
# cipher = result.results["cipher"]
87-
# # this assertion should be true even when TLS is not related to the backend database
88-
# # because by default mysqlrouter will use TLS, unless explicitly disabled, which we never do
89-
# assert cipher == "TLS_AES_256_GCM_SHA384", "Cipher not set"
4+
import asyncio
5+
import logging
6+
import time
7+
8+
import pytest
9+
from pytest_operator.plugin import OpsTest
10+
11+
from .helpers import get_tls_certificate_issuer
12+
13+
logger = logging.getLogger(__name__)
14+
15+
MYSQL_APP_NAME = "mysql"
16+
MYSQL_ROUTER_APP_NAME = "mysqlrouter"
17+
TEST_APP_NAME = "mysql-test-app"
18+
TLS_APP_NAME = "tls-certificates-operator"
19+
SLOW_TIMEOUT = 15 * 60
20+
MODEL_CONFIG = {"logging-config": "<root>=INFO;unit=DEBUG"}
21+
22+
23+
@pytest.mark.group(1)
24+
@pytest.mark.abort_on_fail
25+
async def test_build_deploy_and_relate(ops_test: OpsTest, mysql_router_charm_series: str) -> None:
26+
"""Test encryption when backend database is using TLS."""
27+
await ops_test.model.set_config(MODEL_CONFIG)
28+
logger.info("Deploy and relate all applications")
29+
async with ops_test.fast_forward():
30+
# deploy mysql first
31+
await ops_test.model.deploy(
32+
MYSQL_APP_NAME, channel="8.0/edge", config={"profile": "testing"}, num_units=1
33+
)
34+
tls_config = {"generate-self-signed-certificates": "true", "ca-common-name": "Test CA"}
35+
36+
# ROUTER
37+
mysqlrouter_charm = await ops_test.build_charm(".")
38+
39+
# tls, test app and router
40+
await asyncio.gather(
41+
ops_test.model.deploy(
42+
mysqlrouter_charm,
43+
application_name=MYSQL_ROUTER_APP_NAME,
44+
num_units=None,
45+
series=mysql_router_charm_series,
46+
),
47+
ops_test.model.deploy(
48+
TLS_APP_NAME, application_name=TLS_APP_NAME, channel="stable", config=tls_config
49+
),
50+
ops_test.model.deploy(
51+
TEST_APP_NAME,
52+
application_name=TEST_APP_NAME,
53+
channel="latest/edge",
54+
series=mysql_router_charm_series,
55+
),
56+
)
57+
58+
await ops_test.model.relate(
59+
f"{MYSQL_ROUTER_APP_NAME}:backend-database", f"{MYSQL_APP_NAME}:database"
60+
)
61+
await ops_test.model.relate(
62+
f"{TEST_APP_NAME}:database", f"{MYSQL_ROUTER_APP_NAME}:database"
63+
)
64+
65+
logger.info("Waiting for applications to become active")
66+
# We can safely wait only for test application to be ready, given that it will
67+
# only become active once all the other applications are ready.
68+
await ops_test.model.wait_for_idle([TEST_APP_NAME], status="active", timeout=SLOW_TIMEOUT)
69+
70+
71+
@pytest.mark.group(1)
72+
@pytest.mark.abort_on_fail
73+
async def test_connected_encryption(ops_test: OpsTest) -> None:
74+
"""Test encryption when backend database is using TLS."""
75+
mysqlrouter_unit = ops_test.model.applications[MYSQL_ROUTER_APP_NAME].units[0]
76+
77+
issuer = await get_tls_certificate_issuer(
78+
ops_test,
79+
mysqlrouter_unit.name,
80+
socket="/var/snap/charmed-mysql/common/run/mysqlrouter/mysql.sock",
81+
)
82+
assert (
83+
"Issuer: CN = MySQL_Router_Auto_Generated_CA_Certificate" in issuer
84+
), "Expected mysqlrouter autogenerated CA certificate"
85+
86+
logger.info("Relating TLS with mysqlrouter")
87+
await ops_test.model.relate(TLS_APP_NAME, MYSQL_ROUTER_APP_NAME)
88+
89+
time.sleep(30)
90+
91+
logger.info("Getting certificate issuer after relating with tls operator")
92+
issuer = await get_tls_certificate_issuer(
93+
ops_test,
94+
mysqlrouter_unit.name,
95+
socket="/var/snap/charmed-mysql/common/run/mysqlrouter/mysql.sock",
96+
)
97+
assert "CN = Test CA" in issuer, f"Expected mysqlrouter certificate from {TLS_APP_NAME}"
98+
99+
logger.info("Removing relation TLS with mysqlrouter")
100+
await ops_test.model.applications[MYSQL_ROUTER_APP_NAME].remove_relation(
101+
f"{TLS_APP_NAME}:certificates", f"{MYSQL_ROUTER_APP_NAME}:certificates"
102+
)
103+
104+
time.sleep(30)
105+
issuer = await get_tls_certificate_issuer(
106+
ops_test,
107+
mysqlrouter_unit.name,
108+
socket="/var/snap/charmed-mysql/common/run/mysqlrouter/mysql.sock",
109+
)
110+
assert (
111+
"Issuer: CN = MySQL_Router_Auto_Generated_CA_Certificate" in issuer
112+
), "Expected mysqlrouter autogenerated CA certificate"

0 commit comments

Comments
 (0)