Skip to content

Commit 2e6a723

Browse files
committed
test: New tests to validate postgres password autogeneration
1 parent bfad94d commit 2e6a723

File tree

4 files changed

+192
-0
lines changed

4 files changed

+192
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Database check tests for model catalog
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import pytest
2+
from kubernetes.dynamic import DynamicClient
3+
from ocp_resources.secret import Secret
4+
from pytest_testconfig import config as py_config
5+
from simple_logger.logger import get_logger
6+
from timeout_sampler import TimeoutSampler
7+
8+
from .utils import extract_secret_values
9+
10+
LOGGER = get_logger(name=__name__)
11+
12+
13+
@pytest.fixture(scope="class")
14+
def model_catalog_postgres_secret(admin_client: DynamicClient) -> Secret:
15+
"""Get the model-catalog-postgres secret from model registry namespace"""
16+
return Secret(
17+
client=admin_client,
18+
name="model-catalog-postgres",
19+
namespace=py_config["model_registry_namespace"],
20+
ensure_exists=True,
21+
)
22+
23+
24+
@pytest.fixture(scope="class")
25+
def model_catalog_postgres_secret_values(model_catalog_postgres_secret: Secret) -> dict[str, str]:
26+
"""Capture current values of model-catalog-postgres secret in model registry namespace"""
27+
return extract_secret_values(secret=model_catalog_postgres_secret)
28+
29+
30+
@pytest.fixture(scope="class")
31+
def recreated_model_catalog_postgres_secret(
32+
admin_client: DynamicClient, model_catalog_postgres_secret: Secret
33+
) -> dict[str, str]:
34+
"""Delete model-catalog-postgres secret and wait for it to be recreated"""
35+
model_registry_namespace = py_config["model_registry_namespace"]
36+
resource_name = "model-catalog-postgres"
37+
38+
LOGGER.info(f"Deleting secret {resource_name} in namespace {model_registry_namespace}")
39+
model_catalog_postgres_secret.delete()
40+
41+
# Wait for the secret to be recreated by the operator
42+
LOGGER.info(f"Waiting for secret {resource_name} to be recreated...")
43+
44+
recreated_secret = None
45+
for secret in TimeoutSampler(
46+
wait_timeout=120,
47+
sleep=10,
48+
func=Secret,
49+
client=admin_client,
50+
name=resource_name,
51+
namespace=model_registry_namespace,
52+
):
53+
if secret.exists:
54+
LOGGER.info(f"Secret {resource_name} has been recreated")
55+
recreated_secret = secret
56+
break
57+
58+
return extract_secret_values(secret=recreated_secret)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import pytest
2+
from kubernetes.dynamic import DynamicClient
3+
from simple_logger.logger import get_logger
4+
5+
from tests.model_registry.model_catalog.utils import get_postgres_pod_in_namespace
6+
from tests.model_registry.utils import get_model_catalog_pod, wait_for_model_catalog_pod_created
7+
from utilities.general import wait_for_pods_running
8+
9+
LOGGER = get_logger(name=__name__)
10+
11+
12+
def test_model_catalog_postgres_secret_exists(model_catalog_postgres_secret_values):
13+
"""Test that model-catalog-postgres secret exists and is accessible"""
14+
secret_values = model_catalog_postgres_secret_values
15+
assert secret_values, "model-catalog-postgres secret should exist and be accessible"
16+
17+
# Log the keys (not values) for debugging
18+
LOGGER.info(f"Secret contains keys: {list(secret_values.keys())}")
19+
20+
21+
@pytest.mark.dependency(name="test_model_catalog_postgres_password_recreation")
22+
def test_model_catalog_postgres_password_recreation(
23+
model_catalog_postgres_secret_values, recreated_model_catalog_postgres_secret
24+
):
25+
"""Test that secret recreation generates new password but preserves user/database name"""
26+
# Verify database-name and database-user did NOT change
27+
unchanged_keys = ["database-name", "database-user"]
28+
for key in unchanged_keys:
29+
assert model_catalog_postgres_secret_values[key] == recreated_model_catalog_postgres_secret[key], (
30+
f"{key} should remain the same after secret recreation"
31+
)
32+
33+
# Verify database-password DID change (randomization working)
34+
assert (
35+
model_catalog_postgres_secret_values["database-password"]
36+
!= recreated_model_catalog_postgres_secret["database-password"]
37+
), "database-password should be different after secret recreation (randomized)"
38+
39+
LOGGER.info("Password randomization verified - new password generated on recreation")
40+
41+
42+
@pytest.mark.dependency(depends=["test_model_catalog_postgres_password_recreation"])
43+
def test_model_catalog_pod_ready_after_secret_recreation(admin_client: DynamicClient, model_registry_namespace: str):
44+
"""Test that model catalog pod becomes ready after secret recreation"""
45+
# Wait for model catalog pod to be ready after the secret deletion/recreation
46+
postgres_pod = get_postgres_pod_in_namespace(admin_client=admin_client)
47+
postgres_pod.delete()
48+
model_catalog_pods = get_model_catalog_pod(
49+
client=admin_client,
50+
model_registry_namespace=model_registry_namespace,
51+
)
52+
# We can wait for the pods to reflect updated catalog, however, deleting them ensures the updated config is
53+
# applied immediately.
54+
for pod in model_catalog_pods:
55+
pod.delete()
56+
# After the deletion, we need to wait for the pod to be spinned up and get to ready state.
57+
assert wait_for_model_catalog_pod_created(client=admin_client, model_registry_namespace=model_registry_namespace)
58+
wait_for_pods_running(
59+
admin_client=admin_client, namespace_name=model_registry_namespace, number_of_consecutive_checks=6
60+
)
61+
LOGGER.info("Model catalog pod is ready after secret recreation")
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import base64
2+
import binascii
3+
4+
from kubernetes.dynamic import DynamicClient
5+
from ocp_resources.pod import Pod
6+
from ocp_resources.secret import Secret
7+
from simple_logger.logger import get_logger
8+
from timeout_sampler import TimeoutSampler
9+
10+
from utilities.constants import Timeout
11+
12+
LOGGER = get_logger(name=__name__)
13+
14+
15+
def extract_secret_values(secret: Secret) -> dict[str, str]:
16+
"""Extract and decode secret data values from a Secret object.
17+
18+
Args:
19+
secret: The Secret object to extract values from
20+
21+
Returns:
22+
Dict mapping secret keys to decoded string values
23+
"""
24+
secret_values = {}
25+
if secret.instance.data:
26+
for key, encoded_value in secret.instance.data.items():
27+
try:
28+
decoded_value = base64.b64decode(s=encoded_value).decode(encoding="utf-8")
29+
secret_values[key] = decoded_value
30+
except (binascii.Error, UnicodeDecodeError) as e:
31+
LOGGER.warning(f"Failed to decode secret key '{key}': {e}")
32+
secret_values[key] = encoded_value # Keep encoded if decode fails
33+
34+
LOGGER.info(f"Captured secret with keys: {list(secret_values.keys())}")
35+
return secret_values
36+
37+
38+
def wait_for_model_catalog_pod_ready_after_deletion(client: DynamicClient, model_registry_namespace: str) -> None:
39+
"""Wait for model catalog pod to be ready after secret deletion/recreation.
40+
41+
Args:
42+
client: The Kubernetes client to use
43+
model_registry_namespace: The namespace where model catalog is deployed
44+
45+
Raises:
46+
TimeoutExpiredError: If pod doesn't become ready within timeout
47+
"""
48+
LOGGER.info(f"Waiting for model catalog pod to be ready in namespace {model_registry_namespace}")
49+
50+
# Wait for model catalog pod to be running and ready
51+
for pod in TimeoutSampler(
52+
wait_timeout=Timeout.TIMEOUT_5MIN,
53+
sleep=10,
54+
func=Pod.get,
55+
client=client,
56+
namespace=model_registry_namespace,
57+
label_selector="app.kubernetes.io/name=model-catalog",
58+
):
59+
pods = list(pod)
60+
if pods:
61+
catalog_pod = pods[0] # Get the first (should be only) model catalog pod
62+
63+
# Check if pod is running and has conditions
64+
if catalog_pod.status == Pod.Status.RUNNING and catalog_pod.instance.status.conditions:
65+
for condition in catalog_pod.instance.status.conditions:
66+
if condition.type == "Ready" and condition.status == "True":
67+
LOGGER.info(f"Model catalog pod {catalog_pod.name} is ready")
68+
return
69+
70+
LOGGER.info("Model catalog pod not ready yet, continuing to wait...")
71+
72+
LOGGER.error("Timeout waiting for model catalog pod to become ready")

0 commit comments

Comments
 (0)