Skip to content

Commit 546b7a4

Browse files
Generated commit to update templated files based on rev 63572a8 in stackabletech/operator-templating repo. (#681)
Triggered by: Manual run triggered by: razvan with message [new run-tests script]
1 parent d355b3e commit 546b7a4

File tree

2 files changed

+330
-170
lines changed

2 files changed

+330
-170
lines changed

scripts/run-tests

+329
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
#!/bin/env python
2+
# vim: filetype=python syntax=python tabstop=4 expandtab
3+
4+
import argparse
5+
import collections.abc
6+
import contextlib
7+
import logging
8+
import os
9+
import re
10+
import shutil
11+
import subprocess
12+
import sys
13+
import tempfile
14+
15+
__version__ = "0.0.1"
16+
17+
DESCRIPTION = """
18+
Run integration tests. Call this script from the root of the repository.
19+
20+
Exits with 0 on success, 1 on failure.
21+
22+
Requires the following commands to be installed:
23+
* beku
24+
* stackablectl
25+
* kubectl
26+
* kubectl-kuttl
27+
28+
Examples:
29+
30+
1. Install operators, run all tests and clean up test namespaces:
31+
32+
./scripts/run-tests --parallel 4
33+
34+
2. Install operators but for Airflow use version "0.0.0-pr123" instead of "0.0.0-dev" and run all tests as above:
35+
36+
./scripts/run-tests --operator airflow=0.0.0-pr123 --parallel 4
37+
38+
3. Do not install any operators, run the smoke test suite and keep namespace:
39+
40+
./scripts/run-tests --skip-release --skip-delete --test-suite smoke-latest
41+
42+
4. Run the ldap test(s) from the openshift test suite and keep namespace:
43+
44+
./scripts/run-tests --skip-release --skip-delete --test-suite openshift --test ldap
45+
"""
46+
47+
48+
class TestRunnerException(Exception):
49+
pass
50+
51+
52+
def parse_args(argv: list[str]) -> argparse.Namespace:
53+
"""Parse command line args."""
54+
parser = argparse.ArgumentParser(
55+
description=DESCRIPTION, formatter_class=argparse.RawDescriptionHelpFormatter
56+
)
57+
parser.add_argument(
58+
"--version",
59+
help="Display application version",
60+
action="version",
61+
version=f"%(prog)s {__version__}",
62+
)
63+
64+
parser.add_argument(
65+
"--skip-delete",
66+
help="Do not delete test namespaces.",
67+
action="store_true",
68+
)
69+
70+
parser.add_argument(
71+
"--skip-release",
72+
help="Do not install operators.",
73+
action="store_true",
74+
)
75+
76+
parser.add_argument(
77+
"--parallel",
78+
help="How many tests to run in parallel. Default 2.",
79+
type=int,
80+
required=False,
81+
default=2,
82+
)
83+
84+
parser.add_argument(
85+
"--operator",
86+
help="Patch operator version in release.yaml. Format <operator>=<version>",
87+
nargs="*",
88+
type=cli_parse_operator_args,
89+
default=[],
90+
)
91+
92+
parser.add_argument(
93+
"--test",
94+
help="Kuttl test to run.",
95+
type=str,
96+
required=False,
97+
)
98+
99+
parser.add_argument(
100+
"--test-suite",
101+
help="Name of the test suite to expand. Default: default",
102+
type=str,
103+
required=False,
104+
)
105+
106+
parser.add_argument(
107+
"--log-level",
108+
help="Set log level.",
109+
type=cli_log_level,
110+
required=False,
111+
default=logging.INFO,
112+
)
113+
114+
return parser.parse_args(argv)
115+
116+
117+
def cli_parse_operator_args(args: str) -> tuple[str, str]:
118+
if "=" not in args:
119+
raise argparse.ArgumentTypeError(
120+
f"Invalid operator argument: {args}. Must be in format <operator>=<version>"
121+
)
122+
op, version = args.split("=", maxsplit=1)
123+
return (op, version)
124+
125+
126+
def cli_log_level(cli_arg: str) -> int:
127+
match cli_arg:
128+
case "debug":
129+
return logging.DEBUG
130+
case "info":
131+
return logging.INFO
132+
case "error":
133+
return logging.ERROR
134+
case "warning":
135+
return logging.WARNING
136+
case "critical":
137+
return logging.CRITICAL
138+
case _:
139+
raise argparse.ArgumentTypeError("Invalid log level")
140+
141+
142+
def have_requirements() -> None:
143+
commands = [
144+
("beku", "https://github.com/stackabletech/beku.py"),
145+
(
146+
"stackablectl",
147+
"https://github.com/stackabletech/stackable-cockpit/blob/main/rust/stackablectl/README.md",
148+
),
149+
("kubectl", "https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/"),
150+
("kubectl-kuttl", "https://kuttl.dev/"),
151+
]
152+
153+
err = False
154+
for command, url in commands:
155+
if not shutil.which(command):
156+
logging.error(f'Command "{command}" not found, please install from {url}')
157+
err = True
158+
if err:
159+
raise TestRunnerException()
160+
161+
162+
@contextlib.contextmanager
163+
def release_file(
164+
operators: list[tuple[str, str]] = [],
165+
) -> collections.abc.Generator[str, None, None]:
166+
"""Patch release.yaml with operator versions if needed.
167+
168+
If no --operator is set, the default release file is used.
169+
170+
If an invalid operator name is provided (i.e. one that doesn't exist in the
171+
original release file), a TestRunnerException is raised.
172+
173+
Yields the name of the (potentially patched) release file. This is a temporary
174+
file that will be deleted when the context manager exits.
175+
"""
176+
177+
def _patch():
178+
release_file = os.path.join("tests", "release.yaml")
179+
# Make a copy so we can mutate it without affecting the original
180+
ops_copy = operators.copy()
181+
patched_release = []
182+
with open(release_file, "r") as f:
183+
patch_version = ""
184+
for line in f:
185+
if patch_version:
186+
line = re.sub(":.+$", f": {patch_version}", line)
187+
patch_version = ""
188+
else:
189+
for op, version in ops_copy:
190+
if op in line:
191+
patch_version = version
192+
ops_copy.remove((op, version)) # found an operator to patch
193+
break
194+
patched_release.append(line)
195+
if ops_copy:
196+
# Some --operator args were not found in the release file. This is
197+
# most likely a typo and CI pipelines should terminate early in such
198+
# cases.
199+
logging.error(
200+
f"Operators {', '.join([op for op, _ in ops_copy])} not found in {release_file}"
201+
)
202+
raise TestRunnerException()
203+
with tempfile.NamedTemporaryFile(
204+
mode="w",
205+
delete=False,
206+
delete_on_close=False,
207+
prefix="patched",
208+
) as f:
209+
pcontents = "".join(patched_release)
210+
logging.debug(f"Writing patched release to {f.name}: {pcontents}\n")
211+
f.write(pcontents)
212+
return f.name
213+
214+
release_file = _patch()
215+
try:
216+
yield release_file
217+
except TestRunnerException as e:
218+
logging.error(f"Caught exception: {e}")
219+
raise
220+
finally:
221+
if "patched" in release_file:
222+
try:
223+
logging.debug(f"Removing patched release file : {release_file}")
224+
os.remove(release_file)
225+
except FileNotFoundError | OSError:
226+
logging.error(f"Failed to delete patched release file: {release_file}")
227+
228+
229+
def maybe_install_release(skip_release: bool, release_file: str) -> None:
230+
if skip_release:
231+
logging.debug("Skip release installation")
232+
return
233+
stackablectl_err = ""
234+
try:
235+
stackablectl_cmd = [
236+
"stackablectl",
237+
"release",
238+
"install",
239+
"--release-file",
240+
release_file,
241+
"tests",
242+
]
243+
logging.debug(f"Running : {stackablectl_cmd}")
244+
245+
completed_proc = subprocess.run(
246+
stackablectl_cmd,
247+
capture_output=True,
248+
check=True,
249+
)
250+
# stackablectl doesn't return a non-zero exit code on failure
251+
# so we need to check stderr for errors
252+
stackablectl_err = completed_proc.stderr.decode("utf-8")
253+
if "error" in stackablectl_err.lower():
254+
logging.error(stackablectl_err)
255+
logging.error("stackablectl failed")
256+
raise TestRunnerException()
257+
258+
except subprocess.CalledProcessError:
259+
# in case stackablectl starts returning non-zero exit codes
260+
logging.error(stackablectl_err)
261+
logging.error("stackablectl failed")
262+
raise TestRunnerException()
263+
264+
265+
def gen_tests(test_suite: str) -> None:
266+
try:
267+
beku_cmd = [
268+
"beku",
269+
"--test_definition",
270+
os.path.join("tests", "test-definition.yaml"),
271+
"--kuttl_test",
272+
os.path.join("tests", "kuttl-test.yaml.jinja2"),
273+
"--template_dir",
274+
os.path.join("tests", "templates", "kuttl"),
275+
"--output_dir",
276+
os.path.join("tests", "_work"),
277+
]
278+
if test_suite:
279+
beku_cmd.extend(["--suite", test_suite])
280+
281+
logging.debug(f"Running : {beku_cmd}")
282+
subprocess.run(
283+
beku_cmd,
284+
check=True,
285+
)
286+
except subprocess.CalledProcessError:
287+
logging.error("beku failed")
288+
raise TestRunnerException()
289+
290+
291+
def run_tests(test: str, parallel: int, skip_delete: bool) -> None:
292+
try:
293+
kuttl_cmd = ["kubectl-kuttl", "test"]
294+
if test:
295+
kuttl_cmd.extend(["--test", test])
296+
if parallel:
297+
kuttl_cmd.extend(["--parallel", str(parallel)])
298+
if skip_delete:
299+
kuttl_cmd.extend(["--skip-delete"])
300+
301+
logging.debug(f"Running : {kuttl_cmd}")
302+
303+
subprocess.run(
304+
kuttl_cmd,
305+
cwd="tests/_work",
306+
check=True,
307+
)
308+
except subprocess.CalledProcessError:
309+
logging.error("kuttl failed")
310+
raise TestRunnerException()
311+
312+
313+
def main(argv) -> int:
314+
ret = 0
315+
try:
316+
opts = parse_args(argv[1:])
317+
logging.basicConfig(encoding="utf-8", level=opts.log_level)
318+
have_requirements()
319+
gen_tests(opts.test_suite)
320+
with release_file(opts.operator) as f:
321+
maybe_install_release(opts.skip_release, f)
322+
run_tests(opts.test, opts.parallel, opts.skip_delete)
323+
except TestRunnerException:
324+
ret = 1
325+
return ret
326+
327+
328+
if __name__ == "__main__":
329+
sys.exit(main(sys.argv))

0 commit comments

Comments
 (0)