Skip to content

Commit ea8740c

Browse files
authored
DPE-5210 Common credentials fixture and exec timeout workaround (#493)
* refactor for common credentials fixture in tests * timeout issue workaround * add fixture * lib bump
1 parent a301a4a commit ea8740c

18 files changed

+246
-166
lines changed

lib/charms/mysql/v0/mysql.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def wait_until_mysql_connection(self) -> None:
134134
# Increment this major API version when introducing breaking changes
135135
LIBAPI = 0
136136

137-
LIBPATCH = 69
137+
LIBPATCH = 70
138138

139139
UNIT_TEARDOWN_LOCKNAME = "unit-teardown"
140140
UNIT_ADD_LOCKNAME = "unit-add"
@@ -1536,7 +1536,9 @@ def cluster_metadata_exists(self, from_instance: str) -> bool:
15361536
)
15371537

15381538
try:
1539-
output = self._run_mysqlsh_script("\n".join(check_cluster_metadata_commands))
1539+
output = self._run_mysqlsh_script(
1540+
"\n".join(check_cluster_metadata_commands), timeout=10
1541+
)
15401542
except MySQLClientError:
15411543
logger.warning(f"Failed to check if cluster metadata exists {from_instance=}")
15421544
return False

src/mysql_k8s_helpers.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,7 @@ def _execute_commands(
581581
raise MySQLExecError from None
582582

583583
def _run_mysqlsh_script(
584-
self, script: str, verbose: int = 1, timeout: Optional[int] = None
584+
self, script: str, timeout: Optional[int] = None, verbose: int = 0
585585
) -> str:
586586
"""Execute a MySQL shell script.
587587
@@ -611,8 +611,14 @@ def _run_mysqlsh_script(
611611
MYSQLSH_SCRIPT_FILE,
612612
]
613613

614+
# workaround for timeout not working on pebble exec
615+
# https://github.com/canonical/operator/issues/1329
616+
if timeout:
617+
cmd.insert(0, str(timeout))
618+
cmd.insert(0, "timeout")
619+
614620
try:
615-
process = self.container.exec(cmd, timeout=timeout)
621+
process = self.container.exec(cmd)
616622
stdout, _ = process.wait_output()
617623
return stdout
618624
except ExecError:

tests/integration/conftest.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright 2024 Canonical Ltd.
2+
# See LICENSE file for licensing details.
3+
4+
import logging
5+
6+
import pytest
7+
from pytest_operator.plugin import OpsTest
8+
9+
from constants import SERVER_CONFIG_USERNAME
10+
11+
from . import juju_
12+
from .high_availability.high_availability_helpers import get_application_name
13+
14+
logger = logging.getLogger(__name__)
15+
16+
17+
@pytest.fixture(scope="function")
18+
async def credentials(ops_test: OpsTest):
19+
"""Return the credentials for the MySQL cluster."""
20+
logger.info("Getting credentials for the MySQL cluster")
21+
mysql_app_name = get_application_name(ops_test, "mysql-k8s")
22+
unit = ops_test.model.applications[mysql_app_name].units[0]
23+
credentials = await juju_.run_action(unit, "get-password", username=SERVER_CONFIG_USERNAME)
24+
25+
yield credentials

tests/integration/helpers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ async def get_primary_unit(
159159
return None
160160

161161

162-
async def execute_queries_on_unit(
162+
def execute_queries_on_unit(
163163
unit_address: str,
164164
username: str,
165165
password: str,
@@ -474,7 +474,7 @@ async def retrieve_database_variable_value(
474474
server_config_creds = await get_server_config_credentials(unit)
475475
queries = [f"SELECT @@{variable_name};"]
476476

477-
output = await execute_queries_on_unit(
477+
output = execute_queries_on_unit(
478478
unit_ip, server_config_creds["username"], server_config_creds["password"], queries
479479
)
480480

tests/integration/high_availability/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ def built_charm(ops_test: OpsTest) -> pathlib.Path:
5858
return packed_charm[0].resolve(strict=True)
5959

6060

61-
@pytest.fixture()
62-
async def highly_available_cluster(ops_test: OpsTest) -> None:
61+
@pytest.fixture(scope="module")
62+
async def highly_available_cluster(ops_test: OpsTest):
6363
"""Run the set up for high availability tests.
6464
6565
Args:

tests/integration/high_availability/high_availability_helpers.py

Lines changed: 33 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
generate_random_string,
2727
get_cluster_status,
2828
get_primary_unit,
29-
get_server_config_credentials,
3029
get_unit_address,
3130
is_relation_joined,
3231
scale_application,
@@ -47,30 +46,32 @@
4746
logger = logging.getLogger(__name__)
4847

4948

50-
async def get_max_written_value_in_database(ops_test: OpsTest, unit: Unit) -> int:
49+
async def get_max_written_value_in_database(
50+
ops_test: OpsTest, unit: Unit, credentials: dict
51+
) -> int:
5152
"""Retrieve the max written value in the MySQL database.
5253
5354
Args:
5455
ops_test: The ops test framework
5556
unit: The MySQL unit on which to execute queries on
57+
credentials: The credentials to use to connect to the MySQL database
5658
"""
57-
server_config_credentials = await get_server_config_credentials(unit)
5859
unit_address = await get_unit_address(ops_test, unit.name)
5960

6061
select_max_written_value_sql = [f"SELECT MAX(number) FROM `{DATABASE_NAME}`.`{TABLE_NAME}`;"]
6162

62-
output = await execute_queries_on_unit(
63-
unit_address,
64-
server_config_credentials["username"],
65-
server_config_credentials["password"],
66-
select_max_written_value_sql,
63+
output = execute_queries_on_unit(
64+
unit_address=unit_address,
65+
username=credentials["username"],
66+
password=credentials["password"],
67+
queries=select_max_written_value_sql,
6768
)
6869

6970
return output[0]
7071

7172

7273
def get_application_name(ops_test: OpsTest, application_name_substring: str) -> Optional[str]:
73-
"""Returns the name of the application witt the provided application name.
74+
"""Returns the name of the application with the provided application name.
7475
7576
This enables us to retrieve the name of the deployed application in an existing model.
7677
@@ -354,6 +355,7 @@ async def insert_data_into_mysql_and_validate_replication(
354355
ops_test: OpsTest,
355356
database_name: str,
356357
table_name: str,
358+
credentials: dict,
357359
mysql_units: Optional[List[Unit]] = None,
358360
mysql_application_substring: Optional[str] = "mysql",
359361
) -> str:
@@ -370,7 +372,6 @@ async def insert_data_into_mysql_and_validate_replication(
370372
primary = await get_primary_unit(ops_test, mysql_units[0], mysql_application_name)
371373

372374
# insert some data into the new primary and ensure that the writes get replicated
373-
server_config_credentials = await get_server_config_credentials(primary)
374375
primary_address = await get_unit_address(ops_test, primary.name)
375376

376377
value = generate_random_string(255)
@@ -380,10 +381,10 @@ async def insert_data_into_mysql_and_validate_replication(
380381
f"INSERT INTO `{database_name}`.`{table_name}` (id) VALUES ('{value}')",
381382
]
382383

383-
await execute_queries_on_unit(
384+
execute_queries_on_unit(
384385
primary_address,
385-
server_config_credentials["username"],
386-
server_config_credentials["password"],
386+
credentials["username"],
387+
credentials["password"],
387388
insert_value_sql,
388389
commit=True,
389390
)
@@ -398,10 +399,10 @@ async def insert_data_into_mysql_and_validate_replication(
398399
for unit in mysql_units:
399400
unit_address = await get_unit_address(ops_test, unit.name)
400401

401-
output = await execute_queries_on_unit(
402+
output = execute_queries_on_unit(
402403
unit_address,
403-
server_config_credentials["username"],
404-
server_config_credentials["password"],
404+
credentials["username"],
405+
credentials["password"],
405406
select_value_sql,
406407
)
407408
assert output[0] == value
@@ -412,20 +413,21 @@ async def insert_data_into_mysql_and_validate_replication(
412413

413414

414415
async def clean_up_database_and_table(
415-
ops_test: OpsTest, database_name: str, table_name: str
416+
ops_test: OpsTest, database_name: str, table_name: str, credentials: dict
416417
) -> None:
417418
"""Cleans the database and table created by insert_data_into_mysql_and_validate_replication.
418419
419420
Args:
420421
ops_test: The ops test framework
421422
database_name: The name of the database to drop
422423
table_name: The name of the table to drop
424+
credentials: The credentials to use to connect to the MySQL database
423425
"""
424426
mysql_application_name = get_application_name(ops_test, "mysql")
425427

426-
mysql_unit = ops_test.model.applications[mysql_application_name].units[0]
428+
assert mysql_application_name, "MySQL application not found"
427429

428-
server_config_credentials = await get_server_config_credentials(mysql_unit)
430+
mysql_unit = ops_test.model.applications[mysql_application_name].units[0]
429431

430432
primary = await get_primary_unit(ops_test, mysql_unit, mysql_application_name)
431433
primary_address = await get_unit_address(ops_test, primary.name)
@@ -435,17 +437,18 @@ async def clean_up_database_and_table(
435437
f"DROP DATABASE IF EXISTS `{database_name}`",
436438
]
437439

438-
await execute_queries_on_unit(
440+
execute_queries_on_unit(
439441
primary_address,
440-
server_config_credentials["username"],
441-
server_config_credentials["password"],
442+
credentials["username"],
443+
credentials["password"],
442444
clean_up_database_and_table_sql,
443445
commit=True,
444446
)
445447

446448

447449
async def ensure_all_units_continuous_writes_incrementing(
448450
ops_test: OpsTest,
451+
credentials: dict,
449452
mysql_units: Optional[List[Unit]] = None,
450453
mysql_application_name: Optional[str] = None,
451454
) -> None:
@@ -464,36 +467,24 @@ async def ensure_all_units_continuous_writes_incrementing(
464467

465468
assert primary, "Primary unit not found"
466469

467-
last_max_written_value = await get_max_written_value_in_database(ops_test, primary)
468-
469-
select_all_continuous_writes_sql = [f"SELECT * FROM `{DATABASE_NAME}`.`{TABLE_NAME}`"]
470-
server_config_credentials = await get_server_config_credentials(mysql_units[0])
470+
last_max_written_value = await get_max_written_value_in_database(
471+
ops_test, primary, credentials
472+
)
471473

472-
async with ops_test.fast_forward():
474+
async with ops_test.fast_forward(fast_interval="15s"):
473475
for attempt in Retrying(stop=stop_after_delay(15 * 60), wait=wait_fixed(10)):
474476
with attempt:
475477
# ensure that all units are up to date (including the previous primary)
476478
for unit in mysql_units:
477-
unit_address = await get_unit_address(ops_test, unit.name)
478-
479479
# ensure the max written value is incrementing (continuous writes is active)
480-
max_written_value = await get_max_written_value_in_database(ops_test, unit)
480+
max_written_value = await get_max_written_value_in_database(
481+
ops_test, unit, credentials
482+
)
483+
logger.info(f"{max_written_value=} on unit {unit.name}")
481484
assert (
482485
max_written_value > last_max_written_value
483486
), "Continuous writes not incrementing"
484487

485-
# ensure that the unit contains all values up to the max written value
486-
all_written_values = await execute_queries_on_unit(
487-
unit_address,
488-
server_config_credentials["username"],
489-
server_config_credentials["password"],
490-
select_all_continuous_writes_sql,
491-
)
492-
for number in range(1, max_written_value):
493-
assert (
494-
number in all_written_values
495-
), f"Missing {number} in database for unit {unit.name}"
496-
497488
last_max_written_value = max_written_value
498489

499490

tests/integration/high_availability/test_async_replication.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ async def get_max_written_value(first_model: Model, second_model: Model) -> list
431431
logger.info("Querying max value on all units")
432432
for unit in first_model_units + second_model_units:
433433
address = await get_unit_address(None, unit.name, unit.model)
434-
values = await execute_queries_on_unit(
434+
values = execute_queries_on_unit(
435435
address, credentials["username"], credentials["password"], select_max_written_value_sql
436436
)
437437
results.append(values[0])

tests/integration/high_availability/test_node_drain.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
@pytest.mark.group(1)
3131
@pytest.mark.abort_on_fail
3232
async def test_pod_eviction_and_pvc_deletion(
33-
ops_test: OpsTest, highly_available_cluster, continuous_writes
33+
ops_test: OpsTest, highly_available_cluster, continuous_writes, credentials
3434
) -> None:
3535
"""Test behavior when node drains - pod is evicted and pvs are rotated."""
3636
mysql_application_name = get_application_name(ops_test, "mysql")
@@ -42,7 +42,7 @@ async def test_pod_eviction_and_pvc_deletion(
4242
), "The deployed mysql application is not fully online"
4343

4444
logger.info("Ensuring all units have continuous writes incrementing")
45-
await ensure_all_units_continuous_writes_incrementing(ops_test)
45+
await ensure_all_units_continuous_writes_incrementing(ops_test, credentials=credentials)
4646

4747
mysql_unit = ops_test.model.applications[mysql_application_name].units[0]
4848
primary = await get_primary_unit(ops_test, mysql_unit, mysql_application_name)
@@ -71,4 +71,4 @@ async def test_pod_eviction_and_pvc_deletion(
7171
), "The deployed mysql application is not fully online after primary pod eviction"
7272

7373
logger.info("Ensuring all units have continuous writes incrementing")
74-
await ensure_all_units_continuous_writes_incrementing(ops_test)
74+
await ensure_all_units_continuous_writes_incrementing(ops_test, credentials=credentials)

0 commit comments

Comments
 (0)