Skip to content

Commit 2ca5c47

Browse files
committed
test: [3.3] New tests to validate postgres password autogeneration
Signed-off-by: Debarati Basu-Nag <[email protected]>
1 parent 319ec2c commit 2ca5c47

File tree

5 files changed

+209
-4
lines changed

5 files changed

+209
-4
lines changed

tests/model_registry/model_catalog/db_check/__init__.py

Whitespace-only changes.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import pytest
2+
from kubernetes.dynamic import DynamicClient
3+
from ocp_resources.network_policy import NetworkPolicy
4+
from ocp_resources.secret import Secret
5+
from pytest_testconfig import config as py_config
6+
from simple_logger.logger import get_logger
7+
from timeout_sampler import TimeoutSampler
8+
9+
from .utils import extract_secret_values
10+
11+
LOGGER = get_logger(name=__name__)
12+
13+
14+
@pytest.fixture(scope="class")
15+
def model_catalog_postgres_secret(admin_client: DynamicClient, model_registry_namespace: str) -> Secret:
16+
"""Get the model-catalog-postgres secret from model registry namespace"""
17+
return Secret(
18+
client=admin_client,
19+
name="model-catalog-postgres",
20+
namespace=model_registry_namespace,
21+
ensure_exists=True,
22+
)
23+
24+
25+
@pytest.fixture(scope="class")
26+
def model_catalog_postgres_secret_values(model_catalog_postgres_secret: Secret) -> dict[str, str]:
27+
"""Capture current values of model-catalog-postgres secret in model registry namespace"""
28+
return extract_secret_values(secret=model_catalog_postgres_secret)
29+
30+
31+
@pytest.fixture(scope="class")
32+
def recreated_model_catalog_postgres_secret(
33+
admin_client: DynamicClient, model_catalog_postgres_secret: Secret
34+
) -> dict[str, str]:
35+
"""Delete model-catalog-postgres secret and wait for it to be recreated"""
36+
model_registry_namespace = py_config["model_registry_namespace"]
37+
resource_name = "model-catalog-postgres"
38+
39+
LOGGER.info(f"Deleting secret {resource_name} in namespace {model_registry_namespace}")
40+
model_catalog_postgres_secret.delete()
41+
42+
# Wait for the secret to be recreated by the operator
43+
LOGGER.info(f"Waiting for secret {resource_name} to be recreated...")
44+
45+
recreated_secret = None
46+
for secret in TimeoutSampler(
47+
wait_timeout=120,
48+
sleep=10,
49+
func=Secret,
50+
client=admin_client,
51+
name=resource_name,
52+
namespace=model_registry_namespace,
53+
):
54+
if secret.exists:
55+
LOGGER.info(f"Secret {resource_name} has been recreated")
56+
recreated_secret = secret
57+
break
58+
59+
return extract_secret_values(secret=recreated_secret)
60+
61+
62+
@pytest.fixture(scope="class")
63+
def model_catalog_postgres_network_policy(admin_client: DynamicClient, model_registry_namespace: str) -> NetworkPolicy:
64+
"""Get the model-catalog-postgres NetworkPolicy from model registry namespace"""
65+
return NetworkPolicy(
66+
client=admin_client,
67+
name="model-catalog-postgres",
68+
namespace=model_registry_namespace,
69+
ensure_exists=True,
70+
)
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import pytest
2+
from kubernetes.dynamic import DynamicClient
3+
from ocp_resources.network_policy import NetworkPolicy
4+
from simple_logger.logger import get_logger
5+
from timeout_sampler import TimeoutSampler
6+
7+
from tests.model_registry.model_catalog.utils import get_postgres_pod_in_namespace
8+
from tests.model_registry.utils import (
9+
wait_for_model_catalog_pod_ready_after_deletion,
10+
)
11+
12+
LOGGER = get_logger(name=__name__)
13+
14+
15+
class TestModelCatalogDBSecret:
16+
def test_model_catalog_postgres_secret_exists(self, model_catalog_postgres_secret_values):
17+
"""Test that model-catalog-postgres secret exists and is accessible"""
18+
assert model_catalog_postgres_secret_values, (
19+
f"model-catalog-postgres secret should exist and be accessible: {model_catalog_postgres_secret_values}"
20+
)
21+
22+
@pytest.mark.dependency(name="test_model_catalog_postgres_password_recreation")
23+
def test_model_catalog_postgres_password_recreation(
24+
self, model_catalog_postgres_secret_values, recreated_model_catalog_postgres_secret
25+
):
26+
"""Test that secret recreation generates new password but preserves user/database name"""
27+
# Verify database-name and database-user did NOT change
28+
unchanged_keys = ["database-name", "database-user"]
29+
for key in unchanged_keys:
30+
assert model_catalog_postgres_secret_values[key] == recreated_model_catalog_postgres_secret[key], (
31+
f"{key} should remain the same after secret recreation"
32+
)
33+
34+
# Verify database-password DID change (randomization working)
35+
assert (
36+
model_catalog_postgres_secret_values["database-password"]
37+
!= recreated_model_catalog_postgres_secret["database-password"]
38+
), "database-password should be different after secret recreation (randomized)"
39+
40+
LOGGER.info("Password randomization verified - new password generated on recreation")
41+
42+
@pytest.mark.dependency(depends=["test_model_catalog_postgres_password_recreation"])
43+
def test_model_catalog_pod_ready_after_secret_recreation(
44+
self, admin_client: DynamicClient, model_registry_namespace: str
45+
):
46+
"""Test that model catalog pod becomes ready after secret recreation"""
47+
# delete the postgres pod first
48+
get_postgres_pod_in_namespace(admin_client=admin_client, namespace=model_registry_namespace).delete()
49+
# Wait for model catalog pod to be ready after the secret deletion/recreation
50+
wait_for_model_catalog_pod_ready_after_deletion(
51+
client=admin_client, model_registry_namespace=model_registry_namespace
52+
)
53+
LOGGER.info("Model catalog pod is ready after secret recreation")
54+
55+
56+
class TestModelCatalogDBNetworkPolicy:
57+
def test_postgres_network_policy_exists(self, model_catalog_postgres_network_policy):
58+
"""Test that postgres NetworkPolicy exists and is accessible"""
59+
assert model_catalog_postgres_network_policy.exists, "model-catalog-postgres NetworkPolicy should exist"
60+
61+
def test_postgres_network_policy_restricts_to_port_5432(self, model_catalog_postgres_network_policy):
62+
"""Test that NetworkPolicy only allows TCP 5432 ingress"""
63+
spec = model_catalog_postgres_network_policy.instance.spec
64+
assert "Ingress" in spec.policyTypes, "NetworkPolicy should have Ingress policy type"
65+
assert len(spec.ingress) == 1, "NetworkPolicy should have exactly one ingress rule"
66+
67+
port = spec.ingress[0].ports[0]
68+
assert port.port == 5432, "NetworkPolicy should allow only PostgreSQL port 5432"
69+
assert port.protocol == "TCP", "NetworkPolicy port should use TCP protocol"
70+
71+
def test_postgres_network_policy_allows_only_catalog_pods(self, model_catalog_postgres_network_policy):
72+
"""Test that only model-catalog pods can reach postgres"""
73+
from_selector = model_catalog_postgres_network_policy.instance.spec.ingress[0]["from"][
74+
0
75+
].podSelector.matchLabels
76+
assert from_selector["component"] == "model-catalog", (
77+
"Only model-catalog pods should be allowed to access postgres"
78+
)
79+
80+
@pytest.mark.dependency(name="test_postgres_network_policy_recreation")
81+
def test_postgres_network_policy_recreated_after_deletion(
82+
self,
83+
admin_client: DynamicClient,
84+
model_catalog_postgres_network_policy,
85+
model_registry_namespace: str,
86+
):
87+
"""Test that operator recreates NetworkPolicy after deletion"""
88+
model_catalog_postgres_network_policy.delete()
89+
get_postgres_pod_in_namespace(admin_client=admin_client, namespace=model_registry_namespace).delete()
90+
for np in TimeoutSampler(
91+
wait_timeout=120,
92+
sleep=10,
93+
func=NetworkPolicy,
94+
client=admin_client,
95+
name="model-catalog-postgres",
96+
namespace=model_registry_namespace,
97+
):
98+
if np.exists:
99+
LOGGER.info("NetworkPolicy has been recreated by operator")
100+
break
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import base64
2+
import binascii
3+
4+
from ocp_resources.secret import Secret
5+
from simple_logger.logger import get_logger
6+
7+
LOGGER = get_logger(name=__name__)
8+
9+
10+
def extract_secret_values(secret: Secret) -> dict[str, str]:
11+
"""Extract and decode secret data values from a Secret object.
12+
13+
Args:
14+
secret: The Secret object to extract values from
15+
16+
Returns:
17+
Dict mapping secret keys to decoded string values
18+
"""
19+
secret_values = {}
20+
if secret.instance.data:
21+
for key, encoded_value in secret.instance.data.items():
22+
try:
23+
decoded_value = base64.b64decode(s=encoded_value).decode(encoding="utf-8")
24+
secret_values[key] = decoded_value
25+
except (binascii.Error, UnicodeDecodeError) as e:
26+
LOGGER.warning(f"Failed to decode secret key '{key}': {e}")
27+
secret_values[key] = encoded_value # Keep encoded if decode fails
28+
29+
return secret_values

tests/model_registry/model_catalog/utils.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import Any
22

3+
from kubernetes.dynamic import DynamicClient
34
from simple_logger.logger import get_logger
45

56
from ocp_resources.pod import Pod
@@ -9,25 +10,30 @@
910
LOGGER = get_logger(name=__name__)
1011

1112

12-
def get_postgres_pod_in_namespace(namespace: str = "rhoai-model-registries") -> Pod:
13+
def get_postgres_pod_in_namespace(admin_client: DynamicClient, namespace: str = "rhoai-model-registries") -> Pod:
1314
"""Get the PostgreSQL pod for model catalog database."""
14-
postgres_pods = list(Pod.get(namespace=namespace, label_selector="app.kubernetes.io/name=model-catalog-postgres"))
15+
postgres_pods = list(
16+
Pod.get(
17+
dyn_client=admin_client, namespace=namespace, label_selector="app.kubernetes.io/name=model-catalog-postgres"
18+
)
19+
)
1520
assert postgres_pods, f"No PostgreSQL pod found in namespace {namespace}"
1621
return postgres_pods[0]
1722

1823

19-
def execute_database_query(query: str, namespace: str = "rhoai-model-registries") -> str:
24+
def execute_database_query(query: str, admin_client: DynamicClient, namespace: str = "rhoai-model-registries") -> str:
2025
"""
2126
Execute a SQL query against the model catalog database.
2227
2328
Args:
2429
query: SQL query to execute
30+
admin_client: DynamicClient for Kubernetes API access
2531
namespace: OpenShift namespace containing the PostgreSQL pod
2632
2733
Returns:
2834
Raw database query result as string
2935
"""
30-
postgres_pod = get_postgres_pod_in_namespace(namespace=namespace)
36+
postgres_pod = get_postgres_pod_in_namespace(admin_client=admin_client, namespace=namespace)
3137

3238
return postgres_pod.execute(
3339
command=["psql", "-U", "catalog_user", "-d", "model_catalog", "-c", query],

0 commit comments

Comments
 (0)