Skip to content

Commit ad7521d

Browse files
committed
fix liveness test and add more type hints
1 parent 44858f8 commit ad7521d

6 files changed

Lines changed: 140 additions & 40 deletions

File tree

e2e-tests/conftest.py

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
from typing import Dict
23
import pytest
34
import subprocess
45
import logging
@@ -20,7 +21,7 @@
2021

2122

2223
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
23-
def pytest_runtest_makereport(item, call):
24+
def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo) -> None:
2425
"""Collect K8s resources when a test fails."""
2526
outcome = yield
2627
report = outcome.get_result()
@@ -81,7 +82,7 @@ def setup_env_vars() -> None:
8182

8283

8384
@pytest.fixture(scope="class")
84-
def test_paths(request):
85+
def test_paths(request: pytest.FixtureRequest) -> Dict[str, Path]:
8586
"""Fixture to provide paths relative to the test file."""
8687
test_file = Path(request.fspath)
8788
test_dir = test_file.parent
@@ -159,7 +160,7 @@ def _create_namespace(namespace):
159160

160161

161162
@pytest.fixture(scope="class")
162-
def create_infra(test_paths, create_namespace):
163+
def create_infra(test_paths: Dict[str, Path], create_namespace):
163164
global _current_namespace
164165
created_namespaces = []
165166

@@ -191,7 +192,7 @@ def _create_infra(test_name):
191192
_current_namespace = None
192193

193194
if os.environ.get("SKIP_DELETE") == "1":
194-
logger.info("SKIP_DELETE = 1. Skipping test environment cleanup")
195+
logger.info("SKIP_DELETE=1. Skipping test environment cleanup")
195196
return
196197

197198
def run_cmd(cmd: list[str]) -> None:
@@ -375,7 +376,81 @@ def deploy_cert_manager():
375376

376377

377378
@pytest.fixture(scope="class")
378-
def psmdb_client(test_paths) -> tools.MongoManager:
379+
def deploy_minio():
380+
"""Deploy MinIO and clean up after tests."""
381+
service_name = "minio-service"
382+
bucket = "operator-testing"
383+
384+
logger.info(f"Installing MinIO: {service_name}")
385+
386+
subprocess.run(["helm", "uninstall", service_name], capture_output=True, check=False)
387+
subprocess.run(["helm", "repo", "remove", "minio"], capture_output=True, check=False)
388+
subprocess.run(["helm", "repo", "add", "minio", "https://charts.min.io/"], check=True)
389+
390+
endpoint = f"http://{service_name}:9000"
391+
minio_args = [
392+
"helm", "install", service_name, "minio/minio",
393+
"--version", os.environ.get("MINIO_VER"),
394+
"--set", "replicas=1",
395+
"--set", "mode=standalone",
396+
"--set", "resources.requests.memory=256Mi",
397+
"--set", "rootUser=rootuser",
398+
"--set", "rootPassword=rootpass123",
399+
"--set", "users[0].accessKey=some-access-key",
400+
"--set", "users[0].secretKey=some-secret-key",
401+
"--set", "users[0].policy=consoleAdmin",
402+
"--set", "service.type=ClusterIP",
403+
"--set", "configPathmc=/tmp/",
404+
"--set", "securityContext.enabled=false",
405+
"--set", "persistence.size=2G",
406+
"--set", f"fullnameOverride={service_name}",
407+
"--set", "serviceAccount.create=true",
408+
"--set", f"serviceAccount.name={service_name}-sa",
409+
]
410+
411+
tools.retry(lambda: subprocess.run(minio_args, check=True), max_attempts=10, delay=60)
412+
413+
minio_pod = tools.kubectl_bin(
414+
"get", "pods", f"--selector=release={service_name}",
415+
"-o", "jsonpath={.items[].metadata.name}"
416+
).strip()
417+
tools.wait_pod(minio_pod)
418+
419+
operator_ns = os.environ.get("OPERATOR_NS")
420+
if operator_ns:
421+
namespace = tools.kubectl_bin(
422+
"config", "view", "--minify", "-o", "jsonpath={..namespace}"
423+
).strip()
424+
tools.kubectl_bin(
425+
"create", "svc", "-n", operator_ns, "externalname", service_name,
426+
f"--external-name={service_name}.{namespace}.svc.cluster.local",
427+
"--tcp=9000", check=False
428+
)
429+
430+
logger.info(f"Creating MinIO bucket: {bucket}")
431+
tools.kubectl_bin(
432+
"run", "-i", "--rm", "aws-cli",
433+
"--image=perconalab/awscli", "--restart=Never",
434+
"--", "bash", "-c",
435+
f"AWS_ACCESS_KEY_ID=some-access-key "
436+
f"AWS_SECRET_ACCESS_KEY=some-secret-key "
437+
f"AWS_DEFAULT_REGION=us-east-1 "
438+
f"/usr/bin/aws --no-verify-ssl --endpoint-url {endpoint} s3 mb s3://{bucket}",
439+
)
440+
441+
yield
442+
443+
try:
444+
subprocess.run(
445+
["helm", "uninstall", service_name, "--wait", "--timeout", "60s"],
446+
check=True,
447+
)
448+
except subprocess.CalledProcessError as e:
449+
logger.warning(f"Failed to cleanup minio: {e}")
450+
451+
452+
@pytest.fixture(scope="class")
453+
def psmdb_client(test_paths: Dict[str, Path]) -> tools.MongoManager:
379454
"""Deploy and get the client pod name."""
380455
tools.kubectl_bin("apply", "-f", f"{test_paths['conf_dir']}/client-70.yml")
381456

e2e-tests/finalizer/test_finalizer.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1+
from pathlib import Path
12
import pytest
23
import logging
34

45
from lib import tools
5-
from typing import Dict, Union
6+
from typing import Callable, Dict, Union
67

78
logger = logging.getLogger(__name__)
89

910

1011
@pytest.fixture(scope="class", autouse=True)
11-
def config(create_infra) -> Dict[str, Union[int, str]]:
12+
def config(create_infra: Callable[[str], str]) -> Dict[str, Union[int, str]]:
1213
"""Configuration for tests"""
1314
return {
1415
"namespace": create_infra("finalizer"),
@@ -17,7 +18,7 @@ def config(create_infra) -> Dict[str, Union[int, str]]:
1718

1819

1920
@pytest.fixture(scope="class", autouse=True)
20-
def setup_tests(test_paths):
21+
def setup_tests(test_paths: Dict[str, Path]) -> None:
2122
"""Setup test environment"""
2223
tools.kubectl_bin("apply", "-f", f"{test_paths['conf_dir']}/secrets_with_tls.yml")
2324

@@ -26,13 +27,17 @@ class TestFinalizer:
2627
"""Test MongoDB cluster finalizers"""
2728

2829
@pytest.mark.dependency()
29-
def test_create_cluster(self, config, test_paths):
30+
def test_create_cluster(
31+
self, config: Dict[str, Union[int, str]], test_paths: Dict[str, Path]
32+
) -> None:
3033
tools.apply_cluster(f"{test_paths['test_dir']}/conf/{config['cluster']}.yml")
3134
tools.wait_for_running(f"{config['cluster']}-rs0", 3, False)
3235
tools.wait_for_running(f"{config['cluster']}-cfg", 3)
3336

3437
@pytest.mark.dependency(depends=["TestFinalizer::test_create_cluster"])
35-
def test_kill_primary_should_elect_new_one(self, config, psmdb_client):
38+
def test_kill_primary_should_elect_new_one(
39+
self, config: Dict[str, Union[int, str]], psmdb_client: tools.MongoManager
40+
) -> None:
3641
primary = psmdb_client.get_mongo_primary(
3742
f"clusterAdmin:clusterAdmin123456@{config['cluster']}-rs0.{config['namespace']}",
3843
config["cluster"],
@@ -47,7 +52,7 @@ def test_kill_primary_should_elect_new_one(self, config, psmdb_client):
4752
assert new_primary != primary, "Primary did not change after killing the pod"
4853

4954
@pytest.mark.dependency(depends=["TestFinalizer::test_kill_primary_should_elect_new_one"])
50-
def test_delete_cluster(self, config):
55+
def test_delete_cluster(self, config: Dict[str, Union[int, str]]) -> None:
5156
tools.kubectl_bin("delete", "psmdb", config["cluster"], "--wait=false")
5257
tools.wait_for_delete(f"psmdb/{config['cluster']}")
5358

e2e-tests/init-deploy/test_init_deploy.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
import logging
55

66
from lib import tools
7-
from typing import Dict, Union
7+
from typing import Callable, Dict, Union
88

99
logger = logging.getLogger(__name__)
1010

1111

1212
@pytest.fixture(scope="class", autouse=True)
13-
def config(create_infra) -> Dict[str, Union[int, str]]:
13+
def config(create_infra: Callable[[str], str]) -> Dict[str, Union[int, str]]:
1414
"""Configuration for tests"""
1515
return {
1616
"namespace": create_infra("init-deploy"),

e2e-tests/lib/k8s_collector.py

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@ def __init__(self, namespace: str, custom_resources: Optional[List[str]] = None)
2222

2323
def kubectl(self, *args: str) -> str:
2424
"""Run kubectl command and return stdout"""
25-
result = subprocess.run(
26-
["kubectl", *args], capture_output=True, text=True, check=False
27-
)
25+
result = subprocess.run(["kubectl", *args], capture_output=True, text=True, check=False)
2826
return result.stdout if result.returncode == 0 else ""
2927

3028
def kubectl_ns(self, *args: str) -> str:
@@ -47,12 +45,18 @@ def process_resource(self, resource_type: str) -> None:
4745
print(f"Processing {resource_type}...")
4846
base = f"{self.output_dir}/get/{resource_type}"
4947

50-
self.save(f"{base}/{resource_type}.txt", self.kubectl_ns("get", resource_type, "-o", "wide"))
48+
self.save(
49+
f"{base}/{resource_type}.txt", self.kubectl_ns("get", resource_type, "-o", "wide")
50+
)
5151

5252
for name in self.get_names(resource_type):
53-
self.save(f"{self.output_dir}/describe/{resource_type}_{name}.txt",
54-
self.kubectl_ns("describe", resource_type, name))
55-
self.save(f"{base}/{name}.yaml", self.kubectl_ns("get", resource_type, name, "-o", "yaml"))
53+
self.save(
54+
f"{self.output_dir}/describe/{resource_type}_{name}.txt",
55+
self.kubectl_ns("describe", resource_type, name),
56+
)
57+
self.save(
58+
f"{base}/{name}.yaml", self.kubectl_ns("get", resource_type, name, "-o", "yaml")
59+
)
5660

5761
def extract_pod_logs(self, pod: str) -> None:
5862
"""Extract logs for all containers in a pod"""
@@ -74,8 +78,10 @@ def process_pods(self) -> None:
7478

7579
pods = self.get_names("pods")
7680
for pod in pods:
77-
self.save(f"{self.output_dir}/describe/pod_{pod}.txt",
78-
self.kubectl_ns("describe", "pod", pod))
81+
self.save(
82+
f"{self.output_dir}/describe/pod_{pod}.txt",
83+
self.kubectl_ns("describe", "pod", pod),
84+
)
7985
self.save(f"{base}/{pod}.yaml", self.kubectl_ns("get", "pod", pod, "-o", "yaml"))
8086

8187
with ThreadPoolExecutor(max_workers=5) as executor:
@@ -84,10 +90,12 @@ def process_pods(self) -> None:
8490
def extract_events(self) -> None:
8591
"""Extract namespace events"""
8692
print("Extracting events...")
87-
self.save(f"{self.output_dir}/events/events.txt",
88-
self.kubectl_ns("get", "events", "-o", "wide"))
89-
self.save(f"{self.output_dir}/events/events.json",
90-
self.kubectl_ns("get", "events", "-o", "json"))
93+
self.save(
94+
f"{self.output_dir}/events/events.txt", self.kubectl_ns("get", "events", "-o", "wide")
95+
)
96+
self.save(
97+
f"{self.output_dir}/events/events.json", self.kubectl_ns("get", "events", "-o", "json")
98+
)
9199

92100
def extract_errors(self) -> None:
93101
"""Extract error lines from all logs into summary"""
@@ -102,15 +110,17 @@ def extract_errors(self) -> None:
102110
continue
103111
path = os.path.join(root, file)
104112
with open(path) as f:
105-
error_lines = [l for l in f if "error" in l.lower()]
113+
error_lines = [line for line in f if "error" in line.lower()]
106114
if error_lines:
107115
rel_path = os.path.relpath(path, logs_dir)
108116
errors.append(f"=== {rel_path} ===\n" + "".join(error_lines))
109117

110118
if errors:
111-
self.save(f"{self.output_dir}/error_summary.log",
112-
f"Errors for {self.namespace} ({self.timestamp})\n{'='*50}\n\n" +
113-
"\n\n".join(errors))
119+
self.save(
120+
f"{self.output_dir}/error_summary.log",
121+
f"Errors for {self.namespace} ({self.timestamp})\n{'=' * 50}\n\n"
122+
+ "\n\n".join(errors),
123+
)
114124

115125
def collect_all(self) -> None:
116126
"""Main collection method"""
@@ -159,4 +169,3 @@ def get_namespace(test_log: str) -> str:
159169
print(f"Extracted namespace: {match.group(1)}", file=sys.stderr)
160170
return match.group(1)
161171
raise ValueError("Namespace not found in logs")
162-

e2e-tests/lib/tools.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,9 @@ def get_cr_version() -> str:
463463
"""Get CR version from cr.yaml"""
464464
try:
465465
with open(
466-
os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..", "deploy", "cr.yaml"))
466+
os.path.realpath(
467+
os.path.join(os.path.dirname(__file__), "..", "..", "deploy", "cr.yaml")
468+
)
467469
) as f:
468470
return next(line.split()[1] for line in f if "crVersion" in line)
469471
except (StopIteration, Exception) as e:
@@ -663,10 +665,10 @@ def get_expected_file(test_dir: str | Path, user: str) -> Dict[str, Any]:
663665
else:
664666
raise FileNotFoundError(f"Expected file not found: {base_file}")
665667

666-
def clean_mongo_json(data: Dict[str, Any]):
668+
def clean_mongo_json(data: Dict[str, Any]) -> Dict[str, Any]:
667669
"""Remove timestamps and metadata from MongoDB response"""
668670

669-
def remove_timestamps(obj):
671+
def remove_timestamps(obj: Dict[str, Any]) -> Dict[str, Any]:
670672
if isinstance(obj, dict):
671673
return {
672674
k: remove_timestamps(v)

e2e-tests/liveness/test_liveness.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
#!/usr/bin/env python3
22

3+
from pathlib import Path
34
import pytest
45
import logging
56
import re
67

78
from lib import tools
8-
from typing import Dict
9+
from typing import Callable, Dict
910

1011
logger = logging.getLogger(__name__)
1112

1213

1314
@pytest.fixture(scope="class", autouse=True)
14-
def config(create_infra) -> Dict[str, str]:
15+
def config(create_infra: Callable[[str], str]) -> Dict[str, str]:
1516
"""Configuration for tests"""
1617
return {
1718
"namespace": create_infra("liveness"),
@@ -20,9 +21,15 @@ def config(create_infra) -> Dict[str, str]:
2021

2122

2223
@pytest.fixture(scope="class", autouse=True)
23-
def setup_tests(test_paths):
24+
def setup_tests(test_paths: Dict[str, Path], deploy_minio) -> None:
2425
"""Setup test environment"""
25-
tools.kubectl_bin("apply", "-f", f"{test_paths['conf_dir']}/secrets_with_tls.yml")
26+
tools.kubectl_bin(
27+
"apply",
28+
"-f",
29+
f"{test_paths['conf_dir']}/secrets.yml",
30+
"-f",
31+
f"{test_paths['conf_dir']}/minio-secret.yml",
32+
)
2633

2734

2835
class TestLiveness:
@@ -37,7 +44,7 @@ def test_create_first_cluster(self, config, test_paths):
3744
)
3845

3946
@pytest.mark.dependency(depends=["TestLiveness::test_create_first_cluster"])
40-
def test_liveness_check_fails_with_invalid_ssl_option(self, config):
47+
def test_liveness_check_fails_with_invalid_ssl_option(self, config: Dict[str, str]) -> None:
4148
tools.kubectl_bin(
4249
"exec",
4350
f"{config['cluster']}-rs0-0",
@@ -69,7 +76,9 @@ def test_liveness_check_fails_with_invalid_ssl_option(self, config):
6976
@pytest.mark.dependency(
7077
depends=["TestLiveness::test_liveness_check_fails_with_invalid_ssl_option"]
7178
)
72-
def test_change_liveness_config(self, config, test_paths):
79+
def test_change_liveness_config(
80+
self, config: Dict[str, str], test_paths: Dict[str, Path]
81+
) -> None:
7382
tools.apply_cluster(f"{test_paths['test_dir']}/conf/{config['cluster']}-rs0-changed.yml")
7483

7584
tools.wait_for_running(f"{config['cluster']}-rs0", 3)

0 commit comments

Comments
 (0)