Skip to content

Commit 61db833

Browse files
Add test to ensure correct k8s endpoints created for clusters with the same name (#508)
1 parent a250a69 commit 61db833

File tree

3 files changed

+181
-15
lines changed

3 files changed

+181
-15
lines changed

tests/integration/helpers.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -272,18 +272,36 @@ async def scale_application(
272272
)
273273

274274

275-
def is_relation_joined(ops_test: OpsTest, endpoint_one: str, endpoint_two: str) -> bool:
275+
def is_relation_joined(
276+
ops_test: OpsTest,
277+
endpoint_one: str,
278+
endpoint_two: str,
279+
application_one: Optional[str] = None,
280+
application_two: Optional[str] = None,
281+
) -> bool:
276282
"""Check if a relation is joined.
277283
278284
Args:
279285
ops_test: The ops test object passed into every test case
280286
endpoint_one: The first endpoint of the relation
281287
endpoint_two: The second endpoint of the relation
288+
application_one: The name of the first application
289+
application_two: The name of the second application
282290
"""
283291
for rel in ops_test.model.relations:
284-
endpoints = [endpoint.name for endpoint in rel.endpoints]
285-
if endpoint_one in endpoints and endpoint_two in endpoints:
286-
return True
292+
if application_one and application_two:
293+
endpoints = [
294+
f"{endpoint.application_name}:{endpoint.name}" for endpoint in rel.endpoints
295+
]
296+
if (
297+
f"{application_one}:{endpoint_one}" in endpoints
298+
and f"{application_two}:{endpoint_two}" in endpoints
299+
):
300+
return True
301+
else:
302+
endpoints = [endpoint.name for endpoint in rel.endpoints]
303+
if endpoint_one in endpoints and endpoint_two in endpoints:
304+
return True
287305
return False
288306

289307

tests/integration/high_availability/high_availability_helpers.py

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from lightkube import Client
1818
from lightkube.models.meta_v1 import ObjectMeta
1919
from lightkube.resources.apps_v1 import StatefulSet
20-
from lightkube.resources.core_v1 import PersistentVolume, PersistentVolumeClaim, Pod
20+
from lightkube.resources.core_v1 import Endpoints, PersistentVolume, PersistentVolumeClaim, Pod
2121
from pytest_operator.plugin import OpsTest
2222
from tenacity import RetryError, Retrying, retry, stop_after_attempt, stop_after_delay, wait_fixed
2323

@@ -122,6 +122,7 @@ async def deploy_and_scale_mysql(
122122
mysql_application_name: str = MYSQL_DEFAULT_APP_NAME,
123123
num_units: int = 3,
124124
model: Optional[Model] = None,
125+
cluster_name: str = CLUSTER_NAME,
125126
) -> str:
126127
"""Deploys and scales the mysql application charm.
127128
@@ -132,6 +133,7 @@ async def deploy_and_scale_mysql(
132133
mysql_application_name: The name of the mysql application if it is to be deployed
133134
num_units: The number of units to deploy
134135
model: The model to deploy the mysql application to
136+
cluster_name: The name of the mysql cluster
135137
"""
136138
application_name = get_application_name(ops_test, "mysql")
137139
if not model:
@@ -150,7 +152,7 @@ async def deploy_and_scale_mysql(
150152
# Cache the built charm to avoid rebuilding it between tests
151153
mysql_charm = charm
152154

153-
config = {"cluster-name": CLUSTER_NAME, "profile": "testing"}
155+
config = {"cluster-name": cluster_name, "profile": "testing"}
154156
resources = {"mysql-image": METADATA["resources"]["mysql-image"]["upstream-source"]}
155157

156158
async with ops_test.fast_forward("60s"):
@@ -177,15 +179,21 @@ async def deploy_and_scale_mysql(
177179
return mysql_application_name
178180

179181

180-
async def deploy_and_scale_application(ops_test: OpsTest) -> str:
182+
async def deploy_and_scale_application(
183+
ops_test: OpsTest,
184+
check_for_existing_application: bool = True,
185+
test_application_name: str = APPLICATION_DEFAULT_APP_NAME,
186+
) -> str:
181187
"""Deploys and scales the test application charm.
182188
183189
Args:
184190
ops_test: The ops test framework
191+
check_for_existing_application: Whether to check for existing test applications
192+
test_application_name: Name of test application to be deployed
185193
"""
186-
application_name = get_application_name(ops_test, APPLICATION_DEFAULT_APP_NAME)
194+
application_name = get_application_name(ops_test, test_application_name)
187195

188-
if application_name:
196+
if check_for_existing_application and application_name:
189197
if len(ops_test.model.applications[application_name].units) != 1:
190198
async with ops_test.fast_forward("60s"):
191199
await scale_application(ops_test, application_name, 1)
@@ -195,22 +203,22 @@ async def deploy_and_scale_application(ops_test: OpsTest) -> str:
195203
async with ops_test.fast_forward("60s"):
196204
await ops_test.model.deploy(
197205
APPLICATION_DEFAULT_APP_NAME,
198-
application_name=APPLICATION_DEFAULT_APP_NAME,
206+
application_name=test_application_name,
199207
num_units=1,
200208
channel="latest/edge",
201209
202210
)
203211

204212
await ops_test.model.wait_for_idle(
205-
apps=[APPLICATION_DEFAULT_APP_NAME],
213+
apps=[test_application_name],
206214
status="waiting",
207215
raise_on_blocked=True,
208216
timeout=TIMEOUT,
209217
)
210218

211-
assert len(ops_test.model.applications[APPLICATION_DEFAULT_APP_NAME].units) == 1
219+
assert len(ops_test.model.applications[test_application_name].units) == 1
212220

213-
return APPLICATION_DEFAULT_APP_NAME
221+
return test_application_name
214222

215223

216224
async def relate_mysql_and_application(
@@ -223,13 +231,27 @@ async def relate_mysql_and_application(
223231
mysql_application_name: The mysql charm application name
224232
application_name: The continuous writes test charm application name
225233
"""
226-
if is_relation_joined(ops_test, "database", "database"):
234+
if is_relation_joined(
235+
ops_test,
236+
"database",
237+
"database",
238+
application_one=mysql_application_name,
239+
application_two=application_name,
240+
):
227241
return
228242

229243
await ops_test.model.relate(
230244
f"{application_name}:database", f"{mysql_application_name}:database"
231245
)
232-
await ops_test.model.block_until(lambda: is_relation_joined(ops_test, "database", "database"))
246+
await ops_test.model.block_until(
247+
lambda: is_relation_joined(
248+
ops_test,
249+
"database",
250+
"database",
251+
application_one=mysql_application_name,
252+
application_two=application_name,
253+
)
254+
)
233255

234256
await ops_test.model.wait_for_idle(
235257
apps=[mysql_application_name, application_name],
@@ -669,3 +691,15 @@ def delete_pod(ops_test: OpsTest, unit: Unit) -> None:
669691
],
670692
check=True,
671693
)
694+
695+
696+
def get_endpoint_addresses(ops_test: OpsTest, endpoint_name: str) -> list[str]:
697+
"""Retrieve the addresses selected by a K8s endpoint."""
698+
client = lightkube.Client()
699+
endpoint = client.get(
700+
Endpoints,
701+
namespace=ops_test.model.info.name,
702+
name=endpoint_name,
703+
)
704+
705+
return [address.ip for subset in endpoint.subsets for address in subset.addresses]
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2024 Canonical Ltd.
3+
# See LICENSE file for licensing details.
4+
5+
import logging
6+
7+
import pytest
8+
from pytest_operator.plugin import OpsTest
9+
10+
from ..helpers import get_unit_address
11+
from .high_availability_helpers import (
12+
deploy_and_scale_application,
13+
deploy_and_scale_mysql,
14+
get_endpoint_addresses,
15+
relate_mysql_and_application,
16+
)
17+
18+
logger = logging.getLogger(__name__)
19+
20+
MYSQL_CLUSTER_ONE = "mysql1"
21+
MYSQL_CLUSTER_TWO = "mysql2"
22+
MYSQL_CLUSTER_NAME = "test_cluster"
23+
TEST_APP_ONE = "mysql-test-app1"
24+
TEST_APP_TWO = "mysql-test-app2"
25+
26+
27+
@pytest.mark.group(1)
28+
@pytest.mark.abort_on_fail
29+
async def test_labeling_of_k8s_endpoints(ops_test: OpsTest):
30+
"""Test the labeling of k8s endpoints when apps with same cluster-name deployed."""
31+
logger.info("Deploying first mysql cluster")
32+
mysql_cluster_one = await deploy_and_scale_mysql(
33+
ops_test,
34+
check_for_existing_application=False,
35+
mysql_application_name=MYSQL_CLUSTER_ONE,
36+
cluster_name=MYSQL_CLUSTER_NAME,
37+
)
38+
39+
logger.info("Deploying and relating test app with cluster")
40+
await deploy_and_scale_application(
41+
ops_test,
42+
check_for_existing_application=False,
43+
test_application_name=TEST_APP_ONE,
44+
)
45+
46+
await relate_mysql_and_application(
47+
ops_test,
48+
mysql_application_name=MYSQL_CLUSTER_ONE,
49+
application_name=TEST_APP_ONE,
50+
)
51+
52+
logger.info("Deploying second mysql application with same cluster name")
53+
mysql_cluster_two = await deploy_and_scale_mysql(
54+
ops_test,
55+
check_for_existing_application=False,
56+
mysql_application_name=MYSQL_CLUSTER_TWO,
57+
cluster_name=MYSQL_CLUSTER_NAME,
58+
)
59+
60+
logger.info("Deploying and relating another test app with second cluster")
61+
await deploy_and_scale_application(
62+
ops_test,
63+
check_for_existing_application=False,
64+
test_application_name=TEST_APP_TWO,
65+
)
66+
67+
await relate_mysql_and_application(
68+
ops_test,
69+
mysql_application_name=MYSQL_CLUSTER_TWO,
70+
application_name=TEST_APP_TWO,
71+
)
72+
73+
logger.info("Ensuring that the created k8s endpoints have correct addresses")
74+
cluster_one_ips = [
75+
await get_unit_address(ops_test, unit.name)
76+
for unit in ops_test.model.applications[mysql_cluster_one].units
77+
]
78+
79+
cluster_one_primary_addresses = get_endpoint_addresses(
80+
ops_test, f"{mysql_cluster_one}-primary"
81+
)
82+
cluster_one_replica_addresses = get_endpoint_addresses(
83+
ops_test, f"{mysql_cluster_one}-replicas"
84+
)
85+
86+
for primary in cluster_one_primary_addresses:
87+
assert (
88+
primary in cluster_one_ips
89+
), f"{primary} (not belonging to cluster 1) should not be in cluster one addresses"
90+
91+
assert set(cluster_one_primary_addresses + cluster_one_replica_addresses) == set(
92+
cluster_one_ips
93+
), "IPs not belonging to cluster one in cluster one addresses"
94+
95+
cluster_two_ips = [
96+
await get_unit_address(ops_test, unit.name)
97+
for unit in ops_test.model.applications[mysql_cluster_two].units
98+
]
99+
100+
cluster_two_primary_addresses = get_endpoint_addresses(
101+
ops_test, f"{mysql_cluster_two}-primary"
102+
)
103+
cluster_two_replica_addresses = get_endpoint_addresses(
104+
ops_test, f"{mysql_cluster_two}-replicas"
105+
)
106+
107+
for primary in cluster_two_primary_addresses:
108+
assert (
109+
primary in cluster_two_ips
110+
), f"{primary} (not belonging to cluster w) should not be in cluster two addresses"
111+
112+
assert set(cluster_two_primary_addresses + cluster_two_replica_addresses) == set(
113+
cluster_two_ips
114+
), "IPs not belonging to cluster two in cluster two addresses"

0 commit comments

Comments
 (0)