Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions container_ci_suite/container_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
import logging
import urllib.request
from pathlib import Path
from typing import List, Optional, Literal
from typing import List, Optional, Literal, Union
from datetime import datetime

from container_ci_suite.engines.podman_wrapper import PodmanCLIWrapper
Expand Down Expand Up @@ -360,7 +360,7 @@ def test_db_connection(
username: str = "user",
password: str = "pass",
database: str = "db",
max_attempts: int = 60,
max_attempts: int = 10,
sleep_time: int = 3,
sql_cmd: Optional[str] = None,
) -> bool:
Expand Down Expand Up @@ -613,7 +613,7 @@ def create_container(
self,
cid_file_name: str = "",
container_args: str = "",
docker_args: str = "",
docker_args: Union[list[str], str] = "",
command: str = "",
) -> bool:
"""
Expand All @@ -626,8 +626,11 @@ def create_container(
Returns:
True if container created successfully, False otherwise
"""

if isinstance(container_args, list):
container_args = " ".join(container_args)
if isinstance(docker_args, list):
docker_args = " ".join(docker_args)
if not self.cid_file_dir.exists():
self.cid_file_dir = Path(tempfile.mkdtemp(prefix="cid_files_"))
full_cid_file_name: Path = self.cid_file_dir / cid_file_name
Expand Down Expand Up @@ -1609,6 +1612,7 @@ def build_as_df_build_args(
podman_build_log=self.podman_build_log,
app_id_file_dir=self.app_id_file_dir,
cid_file_dir=self.cid_file_dir,
db_type=self.db_type,
)
except Exception as e:
print(f"S2I build failed: {e}")
Expand Down
133 changes: 102 additions & 31 deletions container_ci_suite/engines/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"""

import logging
import re
import subprocess
import time
from typing import Optional, Literal, Union
Expand Down Expand Up @@ -100,6 +101,50 @@ def __init__(
self.db_type,
)

def wait_for_database(
self,
container_id: str,
command: str,
max_attempts: int = 10,
sleep_time: int = 3,
) -> bool:
"""
Wait for the database to be ready.
Args:
container_id: Container ID or name
command: Command to execute to test if the database is ready
max_attempts: Maximum number of attempts to wait for the database to be ready
sleep_time: Time to sleep between attempts
Returns:
True if database is ready, False otherwise
"""
logger.debug("Waiting for database to be ready...")
logger.debug("Container ID: %s", container_id)
logger.debug("Command: %s", command)
logger.debug("Max attempts: %s", max_attempts)
logger.debug("Sleep time: %s", sleep_time)
for attempt in range(1, max_attempts + 1):
try:
output = PodmanCLIWrapper.podman_exec_shell_command(
cid_file_name=container_id, cmd=command, not_shell=True
)
if isinstance(output, bool) and not output:
logger.debug(
"Database not ready, attempt %s, retrying... (output: '%s')",
attempt,
output,
)
time.sleep(sleep_time)
continue
if isinstance(output, str) and output.strip() == "":
logger.info("Database is ready (output: %s)", output)
return True
except subprocess.CalledProcessError as cpe:
logger.error("Error waiting for database: %s", cpe)
time.sleep(sleep_time)
logger.error("Database not ready after %s attempts", max_attempts)
return False

def assert_login_success(
self,
container_ip: str,
Expand Down Expand Up @@ -369,6 +414,7 @@ def postgresql_cmd(
extra_args: str = "",
sql_command: Optional[str] = None,
podman_run_command: Optional[str] = "run --rm",
docker_args: str = "",
) -> str:
"""
Execute a PostgreSQL command against a container.
Expand All @@ -389,7 +435,8 @@ def postgresql_cmd(
port: Port number (default: 5432)
extra_args: Additional arguments to pass to psql command
sql_command: SQL command to execute (e.g., "-c 'SELECT 1;'")

podman_run_command: Podman run command to use (default: "run --rm")
docker_args: Docker arguments to pass to podman run command
Returns:
Command output as string

Expand All @@ -406,11 +453,12 @@ def postgresql_cmd(
container_id = self.image_name
cmd_parts = [
podman_run_command,
docker_args,
f"-e PGPASSWORD={password}",
self.image_name,
container_id,
"psql",
"-v ON_ERROR_STOP=1",
f"'{connection_string}'",
connection_string,
]

if extra_args:
Expand Down Expand Up @@ -550,11 +598,14 @@ def run_sql_command(
port: int = 3306,
sql_cmd: Optional[Union[list[str], str]] = None,
database: str = "db",
max_attempts: int = 60,
max_attempts: int = 10,
sleep_time: int = 3,
container_id: Optional[str] = None,
docker_args: Optional[Union[list[str], str]] = "",
podman_run_command: Optional[str] = "run --rm",
ignore_error: bool = False,
expected_output: Optional[str] = None,
use_bash: bool = False,
) -> str | bool:
"""
Run a database command inside the container.
Expand All @@ -575,7 +626,8 @@ def run_sql_command(
sleep_time: Time to sleep between attempts (default: 3)
container_id: Container ID or name
podman_run_command: Podman run command to use (default: "run --rm")

ignore_error: Ignore error and return output (default: False)
expected_output: Expected output of the command (default: None)
Returns:
Command output as string or False if command failed
"""
Expand All @@ -585,10 +637,13 @@ def run_sql_command(
sql_cmd = "SELECT 1;"
if isinstance(sql_cmd, str):
sql_cmd = [sql_cmd]
if isinstance(docker_args, list):
docker_args = " ".join(docker_args)
logger.debug(
"Podman run command: %s with image: %s", podman_run_command, container_id
)
logger.debug("Database type: %s", self.db_type)
logger.debug("Docker arguments: %s", docker_args)
logger.debug("SQL command: %s", sql_cmd)
logger.debug("Database: %s", database)
logger.debug("Username: %s", username)
Expand All @@ -599,17 +654,34 @@ def run_sql_command(
logger.debug("Sleep time: %s", sleep_time)
return_output = None
for cmd in sql_cmd:
if use_bash:
cmd = f'bash -c "{cmd}"'
for attempt in range(1, max_attempts + 1):
if self.db_type in ["postgresql", "postgres"]:
return_output = self.postgresql_cmd(
container_ip=container_ip,
username=username,
password=password,
database=database,
sql_command=f"-e '{cmd}'",
container_id=container_id,
podman_run_command=podman_run_command,
)
try:
return_output = self.postgresql_cmd(
container_ip=container_ip,
username=username,
password=password,
database=database,
sql_command=cmd,
container_id=container_id,
podman_run_command=podman_run_command,
docker_args=docker_args,
)
logger.info("PostgreSQL return output: %s", return_output)
except subprocess.CalledProcessError as cpe:
# In case of ignore_error, we return the output
# This is useful for commands that are expected to fail, like wrong login
if ignore_error:
return_output = cpe.output
else:
logger.error(
"Failed to execute command, output: %s, error: %s",
cpe.output,
cpe.stderr,
)
return False
else:
try:
return_output = self.mysql_cmd(
Expand All @@ -621,6 +693,7 @@ def run_sql_command(
container_id=container_id,
podman_run_command=podman_run_command,
)
logger.info("MySQL return output: %s", return_output)
except subprocess.CalledProcessError as cpe:
# In case of ignore_error, we return the output
# This is useful for commands that are expected to fail, like wrong login
Expand All @@ -633,27 +706,25 @@ def run_sql_command(
cpe.stderr,
)
return False
if return_output or return_output == "":
if (
return_output
and expected_output
and re.search(expected_output, return_output)
):
logger.info("Command executed successfully on attempt %s", attempt)
# Let's break out of the loop and return the output
break
if return_output and not expected_output:
logger.info(
"Command executed successfully without checking for expected output on attempt %s"
% attempt
)
break
if attempt < max_attempts:
time.sleep(sleep_time)
else:
if attempt < max_attempts:
logger.debug(
"Attempt %s failed, output: '%s', retrying...",
attempt,
return_output,
)
time.sleep(sleep_time)
else:
logger.error(
"Failed to execute command after %s attempts, output: %s",
max_attempts,
return_output,
)
return False
return False
if return_output:
logger.info("All commands executed successfully")
logger.debug("Output:\n%s", return_output)
logger.debug("Output:\n'%s'", return_output)
return return_output
return False
11 changes: 8 additions & 3 deletions container_ci_suite/engines/podman_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,22 @@ def podman_exec_shell_command(
used_shell: str = "/bin/bash",
return_output: bool = True,
debug: bool = False,
not_shell: bool = False,
):
"""
Function executes shell command if image_name is present in system.
:param cid_file_name: image to check specified by cid_file_name
:param cmd: command that will be executed in image
:param used_shell: which shell will be used /bin/bash or /bin/sh
:param not_shell: if True, the command will be executed without a shell
:return True: In case if image is present
False: In case if image is not present
"""
cmd = f'exec {cid_file_name} {used_shell} -c "{cmd}"'
print(f"podman exec command is: {cmd}")
if not_shell:
cmd = f"exec {cid_file_name} {cmd}"
else:
cmd = f"exec {cid_file_name} {used_shell} -c '{cmd}'"
print(f"podman command is: {cmd}")
try:
output = PodmanCLIWrapper.call_podman_command(
cmd=cmd, return_output=return_output, debug=debug
Expand Down Expand Up @@ -118,7 +123,7 @@ def podman_run_command_and_remove(
:return True: In case if image is present
False: In case if image is not present
"""
cmd = f'run --rm {cid_file_name} /bin/bash -c "{cmd}"'
cmd = f"run --rm {cid_file_name} /bin/bash -c '{cmd}'"
print(f"podman command is: '{cmd}'")
try:
output = PodmanCLIWrapper.call_podman_command(
Expand Down