Skip to content

Commit c92a89c

Browse files
builderphracek
authored andcommitted
Let' improve database engine so we can call
several sql commands at once during the tests. Signed-off-by: builder <build@localhost>
1 parent 550686e commit c92a89c

File tree

3 files changed

+165
-50
lines changed

3 files changed

+165
-50
lines changed

container_ci_suite/container_lib.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,10 +394,14 @@ def assert_container_creation_fails(
394394
if self.create_container(
395395
cid_file_name=cid_file_name, container_args=container_args, command=command
396396
):
397+
logging.info(f"Container creation succeeded for {cid_file_name}")
397398
container_id = self.get_cid(cid_file_name)
398399
attempt = 1
399400
while attempt <= max_attempts:
400401
if not ContainerImage.is_container_running(container_id):
402+
logging.info(
403+
f"Container {container_id} is not running after {attempt} attempts."
404+
)
401405
break
402406
time.sleep(2)
403407
attempt += 1
@@ -412,6 +416,7 @@ def assert_container_creation_fails(
412416
cmd=f"inspect -f '{{{{.State.ExitCode}}}}' {container_id}",
413417
return_output=True,
414418
).strip()
419+
logging.info(f"Exit status for {container_id} is {exit_status}")
415420
if exit_status == "0":
416421
return False
417422
except subprocess.CalledProcessError:
@@ -629,7 +634,10 @@ def create_container(
629634
try:
630635
cmd = f"run {docker_args} --cidfile={full_cid_file_name} -d {container_args} {self.image_name} {command}"
631636
logging.info(f"Command to create container is '{cmd}'.")
632-
PodmanCLIWrapper.call_podman_command(cmd=cmd, return_output=True)
637+
ret_value = PodmanCLIWrapper.call_podman_command(
638+
cmd=cmd, return_output=True
639+
)
640+
logging.info(f"Return value for command '{cmd}' is '{ret_value}'.")
633641
if not ContainerImage.wait_for_cid(cid_file_name=full_cid_file_name):
634642
return False
635643
container_id = utils.get_file_content(full_cid_file_name).strip()

container_ci_suite/engines/database.py

Lines changed: 155 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
import logging
3838
import subprocess
3939
import time
40-
from typing import Optional, Literal
40+
from typing import Optional, Literal, Union
4141
from enum import Enum
4242

4343
from container_ci_suite.engines.podman_wrapper import PodmanCLIWrapper
@@ -95,8 +95,9 @@ def __init__(
9595
self.image_name = image_name
9696
self.db_type = db_type.lower()
9797
logger.debug(
98-
f"DatabaseWrapper initialized with image: {image_name}, "
99-
f"type: {self.db_type}"
98+
"DatabaseWrapper initialized with image: %s, type: %s",
99+
image_name,
100+
self.db_type,
100101
)
101102

102103
def assert_login_success(
@@ -180,8 +181,11 @@ def assert_login_access(
180181
>>> assert db.assert_login_access("172.17.0.2", "user", "wrong", False)
181182
"""
182183
logger.info(
183-
f"Testing {self.db_type} login as {username}:{password}; "
184-
f"expected_success={expected_success}"
184+
"Testing %s login as %s:%s; expected_success=%s",
185+
self.db_type,
186+
username,
187+
password,
188+
expected_success,
185189
)
186190

187191
try:
@@ -195,22 +199,22 @@ def assert_login_access(
195199
)
196200

197201
if success and expected_success:
198-
logger.info(f" {username}({password}) access granted as expected")
202+
logger.info(" %s(%s) access granted as expected", username, password)
199203
return True
200204
elif not success and not expected_success:
201-
logger.info(f" {username}({password}) access denied as expected")
205+
logger.info(" %s(%s) access denied as expected", username, password)
202206
return True
203207
else:
204-
logger.error(f" {username}({password}) login assertion failed")
208+
logger.error(" %s(%s) login assertion failed", username, password)
205209
return False
206210

207211
except Exception as e:
208-
logger.error(f"Error during login test: {e}")
212+
logger.error("Error during login test: %s", e)
209213
if not expected_success:
210-
logger.info(f" {username}({password}) access denied as expected")
214+
logger.info(" %s(%s) access denied as expected", username, password)
211215
return True
212216
else:
213-
logger.error(f" {username}({password}) login assertion failed")
217+
logger.error(" %s(%s) login assertion failed", username, password)
214218
return False
215219

216220
def _test_mysql_login(
@@ -287,6 +291,8 @@ def mysql_cmd(
287291
port: int = 3306,
288292
extra_args: str = "",
289293
sql_command: Optional[str] = None,
294+
container_id: Optional[str] = None,
295+
podman_run_command: Optional[str] = "run --rm",
290296
) -> str:
291297
"""
292298
Execute a MySQL command against a container.
@@ -310,7 +316,8 @@ def mysql_cmd(
310316
port: Port number (default: 3306)
311317
extra_args: Additional arguments to pass to mysql command
312318
sql_command: SQL command to execute (e.g., "-e 'SELECT 1;'")
313-
319+
podman_run_command: Podman run command to use (default: "run --rm")
320+
ignore_error: Ignore error and return output (default: False)
314321
Returns:
315322
Command output as string
316323
@@ -322,9 +329,13 @@ def mysql_cmd(
322329
>>> output = db.mysql_cmd("172.17.0.2", "user", "pass",
323330
... sql_command="-e 'SELECT 1;'")
324331
"""
332+
if not container_id:
333+
container_id = self.image_name
334+
if not sql_command:
335+
sql_command = "-e 'SELECT 1;'"
325336
cmd_parts = [
326-
"run --rm",
327-
self.image_name,
337+
podman_run_command,
338+
container_id,
328339
"mysql",
329340
f"--host {container_ip}",
330341
f"--port {port}",
@@ -341,19 +352,23 @@ def mysql_cmd(
341352
cmd_parts.append(database)
342353

343354
cmd = " ".join(cmd_parts)
344-
logging.debug(f"Executing command: {cmd}")
355+
logging.debug("Executing command: %s", cmd)
345356

346-
return PodmanCLIWrapper.call_podman_command(cmd=cmd, return_output=True)
357+
return PodmanCLIWrapper.call_podman_command(
358+
cmd=cmd, return_output=True, ignore_error=False
359+
)
347360

348361
def postgresql_cmd(
349362
self,
350363
container_ip: str,
351364
username: str,
352365
password: str,
366+
container_id: Optional[str] = None,
353367
database: str = "db",
354368
port: int = 5432,
355369
extra_args: str = "",
356370
sql_command: Optional[str] = None,
371+
podman_run_command: Optional[str] = "run --rm",
357372
) -> str:
358373
"""
359374
Execute a PostgreSQL command against a container.
@@ -387,9 +402,10 @@ def postgresql_cmd(
387402
... sql_command="-c 'SELECT 1;'")
388403
"""
389404
connection_string = f"postgresql://{username}@{container_ip}:{port}/{database}"
390-
405+
if not container_id:
406+
container_id = self.image_name
391407
cmd_parts = [
392-
"run --rm",
408+
podman_run_command,
393409
f"-e PGPASSWORD={password}",
394410
self.image_name,
395411
"psql",
@@ -432,7 +448,7 @@ def test_connection(
432448
port: Port number (default: 3306 for MySQL, 5432 for PostgreSQL)
433449
max_attempts: Maximum number of connection attempts (default: 60)
434450
sleep_time: Seconds to wait between attempts (default: 3)
435-
451+
sql_cmd: SQL command to execute (e.g., "SELECT 1;")
436452
Returns:
437453
True if connection successful, False otherwise
438454
@@ -441,7 +457,7 @@ def test_connection(
441457
>>> if db.test_connection("172.17.0.2", "user", "pass"):
442458
... print("Database is ready!")
443459
"""
444-
logger.info(f"Testing {self.db_type} connection to {container_ip}...")
460+
logger.info("Testing %s connection to %s...", self.db_type, container_ip)
445461
logger.info("Trying to connect...")
446462
for attempt in range(1, max_attempts + 1):
447463
try:
@@ -463,16 +479,16 @@ def test_connection(
463479
database=database,
464480
sql_command=sql_cmd,
465481
)
466-
logging.debug(f"Output: {return_output}")
467-
logger.info(f"Connection successful on attempt {attempt}")
482+
logging.debug("Output: %s", return_output)
483+
logger.info("Connection successful on attempt %s", attempt)
468484
return True
469485

470486
except subprocess.CalledProcessError:
471487
if attempt < max_attempts:
472-
logger.debug(f"Attempt {attempt} failed, retrying...")
488+
logger.debug("Attempt %s failed, retrying...", attempt)
473489
time.sleep(sleep_time)
474490
else:
475-
logger.error(f"Failed to connect after {max_attempts} attempts")
491+
logger.error("Failed to connect after %s attempts", max_attempts)
476492
return False
477493

478494
return False
@@ -526,27 +542,118 @@ def assert_local_access(self, container_id: str, username: str = None) -> bool:
526542
logger.error(" Local access assertion failed")
527543
return False
528544

529-
# def run_db_command(
530-
# self, container_id: str, username: str, password: str, db_command: str
531-
# ) -> str:
532-
# """
533-
# Run a database command inside the container.
534-
535-
# Args:
536-
# container_id: Container ID or name
537-
# command: Command to run like mysql or psql
538-
539-
# Returns:
540-
# Command output as string
541-
# """
542-
# if self.db_type in ["postgresql", "postgres"]:
543-
# db_cmd = (
544-
# f"psql -h {container_ip} -u{username} -p{password} -e '{db_command};'"
545-
# )
546-
# else:
547-
# db_cmd = (
548-
# f"mysql -h {container_ip} -u{username} -p{password} -e '{db_command};'"
549-
# )
550-
# return PodmanCLIWrapper.call_podman_command(
551-
# cmd=f"run --rm {container_id} {db_cmd}", return_output=True
552-
# )
545+
def run_sql_command(
546+
self,
547+
username: Optional[str] = None,
548+
password: Optional[str] = None,
549+
container_ip: str = None,
550+
port: int = 3306,
551+
sql_cmd: Optional[Union[list[str], str]] = None,
552+
database: str = "db",
553+
max_attempts: int = 60,
554+
sleep_time: int = 3,
555+
container_id: Optional[str] = None,
556+
podman_run_command: Optional[str] = "run --rm",
557+
ignore_error: bool = False,
558+
) -> str | bool:
559+
"""
560+
Run a database command inside the container.
561+
562+
Bash equivalent:
563+
```bash
564+
docker exec -i $(get_cid "$id") bash -c psql <<< "SELECT 1;"
565+
```
566+
567+
Args:
568+
username: Username to test with (default: "root" for MySQL, None for PostgreSQL)
569+
password: Password to test with
570+
container_ip: IP address of the container
571+
sql_cmd: SQL command to execute (e.g., "SELECT 1;")
572+
database: Database name (default: "db")
573+
port: Port number (default: 3306 for MySQL, 5432 for PostgreSQL)
574+
max_attempts: Maximum number of attempts (default: 60)
575+
sleep_time: Time to sleep between attempts (default: 3)
576+
container_id: Container ID or name
577+
podman_run_command: Podman run command to use (default: "run --rm")
578+
579+
Returns:
580+
Command output as string or False if command failed
581+
"""
582+
if not container_id:
583+
container_id = self.image_name
584+
if not sql_cmd:
585+
sql_cmd = "SELECT 1;"
586+
if isinstance(sql_cmd, str):
587+
sql_cmd = [sql_cmd]
588+
logger.debug(
589+
"Podman run command: %s with image: %s", podman_run_command, container_id
590+
)
591+
logger.debug("Database type: %s", self.db_type)
592+
logger.debug("SQL command: %s", sql_cmd)
593+
logger.debug("Database: %s", database)
594+
logger.debug("Username: %s", username)
595+
logger.debug("Password: %s", password)
596+
logger.debug("Container IP: %s", container_ip)
597+
logger.debug("Port: %s", port)
598+
logger.debug("Max attempts: %s", max_attempts)
599+
logger.debug("Sleep time: %s", sleep_time)
600+
return_output = None
601+
for cmd in sql_cmd:
602+
for attempt in range(1, max_attempts + 1):
603+
if self.db_type in ["postgresql", "postgres"]:
604+
return_output = self.postgresql_cmd(
605+
container_ip=container_ip,
606+
username=username,
607+
password=password,
608+
database=database,
609+
sql_command=f"-e '{cmd}'",
610+
container_id=container_id,
611+
podman_run_command=podman_run_command,
612+
)
613+
else:
614+
try:
615+
return_output = self.mysql_cmd(
616+
container_ip=container_ip,
617+
username=username,
618+
password=password,
619+
database=database,
620+
sql_command=f"-e '{cmd}'",
621+
container_id=container_id,
622+
podman_run_command=podman_run_command,
623+
)
624+
except subprocess.CalledProcessError as cpe:
625+
# In case of ignore_error, we return the output
626+
# This is useful for commands that are expected to fail, like wrong login
627+
if ignore_error:
628+
return_output = cpe.output
629+
else:
630+
logger.error(
631+
"Failed to execute command, output: %s, error: %s",
632+
cpe.output,
633+
cpe.stderr,
634+
)
635+
return False
636+
if return_output or return_output == "":
637+
logger.info("Command executed successfully on attempt %s", attempt)
638+
# Let's break out of the loop and return the output
639+
break
640+
else:
641+
if attempt < max_attempts:
642+
logger.debug(
643+
"Attempt %s failed, output: '%s', retrying...",
644+
attempt,
645+
return_output,
646+
)
647+
time.sleep(sleep_time)
648+
else:
649+
logger.error(
650+
"Failed to execute command after %s attempts, output: %s",
651+
max_attempts,
652+
return_output,
653+
)
654+
return False
655+
if return_output:
656+
logger.info("All commands executed successfully")
657+
logger.debug("Output:\n%s", return_output)
658+
return return_output
659+
return False

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def get_requirements():
4343
description="A python3 container CI tool for testing images.",
4444
long_description=long_description,
4545
long_description_content_type="text/markdown",
46-
version="0.11.0",
46+
version="0.11.1",
4747
keywords="tool,containers,images,tests",
4848
packages=find_packages(exclude=["tests"]),
4949
url="https://github.com/sclorg/container-ci-suite",

0 commit comments

Comments
 (0)