Skip to content

Commit 06b1c37

Browse files
committed
Introduce k8s_test.py in analogy to openstack_test.py
Signed-off-by: Matthias Büchse <matthias.buechse@alasca.cloud>
1 parent d9aa640 commit 06b1c37

5 files changed

Lines changed: 217 additions & 47 deletions

File tree

Tests/iaas/openstack_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ def __init__(self, level=logging.NOTSET, log=None):
249249
self.log = [] if log is None else log
250250

251251
def handle(self, record):
252-
self.log.append(f'{record.levelname}: {record.msg}')
252+
self.log.append(f'{record.levelname}: {record.getMessage()}')
253253

254254

255255
def main(argv):

Tests/kaas/k8s_test.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
#!/usr/bin/env python3
2+
# vim: set ts=4 sw=4 et:
3+
4+
"""Kubernetes testcase runner
5+
6+
(c) Matthias Büchse <matthias.buechse@alasca.cloud>, 7/2026
7+
SPDX-License-Identifier: CC-BY-SA 4.0
8+
"""
9+
10+
from datetime import datetime
11+
import getopt
12+
import logging
13+
import os
14+
import os.path
15+
import sys
16+
import uuid
17+
18+
import yaml
19+
20+
from scs_0210_version_policy.k8s_version_policy import version_policy_check
21+
from sonobuoy_handler.sonobuoy_handler import SonobuoyHandler
22+
23+
24+
HERE = os.path.dirname(__file__)
25+
SCS_SONOBUOY_CONFIG_PATH = os.path.join(HERE, 'scs-sonobuoy-config-v1.yaml')
26+
27+
logger = logging.getLogger(__name__)
28+
29+
30+
def usage(rcode=1, file=sys.stderr):
31+
"""help output"""
32+
print("Usage: k8s_test.py [options] testcase-id1 ... testcase-idN", file=file)
33+
print("Options:", file=file)
34+
print(" [-k/--kubeconfig KUBECONFIG_PATH] (required)", file=file)
35+
print(" [-s/--subject SUBJECT]", file=file)
36+
print(" [--execution-mode MODE] Sonobuoy mode: serial, parallel, or dry", file=file)
37+
print("Runs specified testcases against the default context in kubeconfig provided", file=file)
38+
print("via -k. Reports inconsistencies, errors etc.; returns 0 on success.", file=file)
39+
print("Instead of listing testcase-ids, you can supply a single dash (-)", file=file)
40+
print("to have them read from stdin, one testcase-id per line.", file=file)
41+
sys.exit(rcode)
42+
43+
44+
class Config:
45+
def __init__(self):
46+
self.kubeconfig_path = os.environ.get("KUBECONFIG")
47+
self.subject = None
48+
self.mode = 'serial'
49+
self.testcases = []
50+
51+
def apply_argv(self, argv):
52+
"""Parse cli arguments from the script call"""
53+
try:
54+
opts, args = getopt.gnu_getopt(argv, "k:s:", ("kubeconfig=", "subject=", "execution-mode="))
55+
except getopt.GetoptError as exc:
56+
print(f"CRITICAL: {exc!r}", file=sys.stderr)
57+
usage(1)
58+
for opt in opts:
59+
if opt[0] == "-h" or opt[0] == "--help":
60+
usage(0)
61+
elif opt[0] == "-k" or opt[0] == "--kubeconfig":
62+
self.kubeconfig_path = opt[1]
63+
elif opt[0] == "-s" or opt[0] == "--subject":
64+
self.subject = opt[1]
65+
elif opt[0] == "--execution-mode":
66+
self.mode = opt[1]
67+
else:
68+
usage(2)
69+
if len(args) == 1 and args[0] == '-':
70+
args = sys.stdin.read().splitlines()
71+
self.testcases = [t for t in args if t in TESTCASES]
72+
if len(self.testcases) != len(args):
73+
unknown = [a for a in args if a not in self.testcases]
74+
logger.warning(f"ignoring unknown testcases: {','.join(unknown)}")
75+
if not self.kubeconfig_path:
76+
print("CRITICAL: You need to have KUBECONFIG set or pass --kubeconfig=KUBECONFIG.", file=sys.stderr)
77+
sys.exit(1)
78+
if not self.subject:
79+
self.subject = self.kubeconfig_path
80+
81+
def compute_sono_args(self, *args):
82+
if self.mode == 'parallel':
83+
# This is merely a shortcut to simplify commandline in scs-compatible-kaas.yaml
84+
# For more on parallel execution, see https://github.com/vmware-tanzu/sonobuoy/issues/1435
85+
args += ('--e2e-parallel=true', )
86+
elif self.mode == 'dry':
87+
# mutually exclusive with parallel, it turns out!
88+
args += ('--plugin-env e2e.E2E_DRYRUN=true', )
89+
return args + ('--plugin-env e2e.E2E_EXTRA_ARGS=--ginkgo.flake-attempts=2', )
90+
91+
92+
def run_sono(config, testcase, *args):
93+
return SonobuoyHandler(
94+
SCS_SONOBUOY_CONFIG_PATH, testcase, config.kubeconfig_path,
95+
args=config.compute_sono_args(*args),
96+
).run()
97+
98+
99+
TESTCASES = {
100+
'cncf-k8s-conformance': lambda config, name: run_sono(config, name, '--mode=certified-conformance'),
101+
'kaas-networking-check': lambda config, name: run_sono(config, name, '--e2e-focus "NetworkPolicy"'),
102+
'version-policy-check': lambda config, _: version_policy_check(config.kubeconfig_path),
103+
}
104+
105+
106+
def harness(name, results, *check_fns):
107+
"""Harness for evaluating testcase `name`.
108+
109+
Logs beginning and end of computation.
110+
Calls each fn in `check_fns`.
111+
Records result to `results`.
112+
"""
113+
logger.info(f'*** {name}')
114+
try:
115+
result = all(check_fn() for check_fn in check_fns)
116+
except BaseException:
117+
logger.debug('exception during check', exc_info=True)
118+
value = 0
119+
else:
120+
value = 1 if result else -1
121+
result = ['FAIL', 'ABORT', 'PASS'][value + 1]
122+
logger.info(f'+++ {name}: {result}')
123+
results[name] = value
124+
125+
126+
def run_preflight_checks(kubeconfig):
127+
# make sure that we can connect to the cloud
128+
pass
129+
130+
131+
class _LogHandler(logging.Handler):
132+
def __init__(self, level=logging.NOTSET, log=None):
133+
super().__init__(level=level)
134+
self.log = [] if log is None else log
135+
136+
def handle(self, record):
137+
self.log.append(f'{record.levelname}: {record.getMessage()}')
138+
139+
140+
def main(argv):
141+
# configure logging, disable verbose library logging
142+
logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG)
143+
144+
config = Config()
145+
config.apply_argv(argv)
146+
147+
try:
148+
run_preflight_checks(config.kubeconfig_path)
149+
except Exception:
150+
logger.critical("Pre-flight checks failed.")
151+
raise
152+
153+
results = {}
154+
log = []
155+
logging.root.addHandler(_LogHandler(level=logging.DEBUG, log=log))
156+
for testcase in config.testcases:
157+
harness(testcase, results, lambda: TESTCASES[testcase](config, testcase))
158+
report = {
159+
'uuid': str(uuid.uuid4()),
160+
'creator': 'k8s_test.py v0.1.0',
161+
'checked_at': datetime.now(),
162+
'subject': config.subject,
163+
'scope': '1fffebe6-fd4b-44d3-a36c-fc58b4bb0180',
164+
'tests': {
165+
key: {'result': value}
166+
for key, value in results.items()
167+
},
168+
'log': log,
169+
}
170+
# don't do explicit_start here because that can easily be done by the caller using "echo ---",
171+
# and then the caller can even add fields such as uuid, subject, and scope
172+
yaml.safe_dump(report, sys.stdout, default_flow_style=False, sort_keys=False, explicit_start=False)
173+
return 0
174+
175+
176+
if __name__ == "__main__":
177+
try:
178+
sys.exit(main(sys.argv[1:]))
179+
except SystemExit:
180+
raise
181+
except BaseException as exc:
182+
print(f"CRITICAL: {exc!r}", file=sys.stderr)
183+
sys.exit(1)

Tests/kaas/scs_0210_version_policy/k8s_version_policy.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,25 @@ def compute_version_policy_check(supported_branches, cve_affected_ranges, releas
435435
return False
436436

437437

438-
async def main(argv):
438+
async def version_policy_check_async(kubeconfig):
439+
supported_branches = determine_supported_branches()
440+
connector = aiohttp.TCPConnector(limit=5)
441+
logger.info("Checking cluster specified in %s.", kubeconfig)
442+
async with aiohttp.ClientSession(connector=connector) as session:
443+
cve_affected_ranges, releases_data, cluster = await asyncio.gather(
444+
collect_cve_versions(session),
445+
fetch_k8s_releases_data(session),
446+
get_k8s_cluster_info(kubeconfig),
447+
)
448+
return compute_version_policy_check(
449+
supported_branches, cve_affected_ranges, releases_data, cluster)
450+
451+
452+
def version_policy_check(kubeconfig):
453+
return asyncio.run(version_policy_check_async(kubeconfig))
454+
455+
456+
def main(argv):
439457
config = Config()
440458
try:
441459
config.apply_argv(argv)
@@ -448,17 +466,7 @@ async def main(argv):
448466
return 1
449467

450468
try:
451-
supported_branches = determine_supported_branches()
452-
connector = aiohttp.TCPConnector(limit=5)
453-
logger.info("Checking cluster specified in %s.", config.kubeconfig)
454-
async with aiohttp.ClientSession(connector=connector) as session:
455-
cve_affected_ranges, releases_data, cluster = await asyncio.gather(
456-
collect_cve_versions(session),
457-
fetch_k8s_releases_data(session),
458-
get_k8s_cluster_info(config.kubeconfig),
459-
)
460-
result = compute_version_policy_check(
461-
supported_branches, cve_affected_ranges, releases_data, cluster)
469+
result = version_policy_check(config.kubeconfig)
462470
print("version-policy-check: " + ('FAIL', 'PASS')[bool(result)])
463471
except BaseException:
464472
print("version-policy-check: ABORT")
@@ -469,5 +477,5 @@ async def main(argv):
469477

470478

471479
if __name__ == "__main__":
472-
return_code = asyncio.run(main(sys.argv[1:]))
480+
return_code = main(sys.argv[1:])
473481
sys.exit(return_code)

Tests/kaas/sonobuoy_handler/sonobuoy_handler.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import shutil
88
import subprocess
99
import sys
10+
from tempfile import gettempdir
1011

1112
import yaml
1213

@@ -43,7 +44,7 @@ def __init__(
4344
scs_sonobuoy_config_yaml,
4445
check_name="sonobuoy_handler",
4546
kubeconfig=None,
46-
result_dir_name="sonobuoy_results",
47+
result_dir_name=None,
4748
args=(),
4849
):
4950
self.check_name = check_name
@@ -52,6 +53,8 @@ def __init__(
5253
raise RuntimeError("No kubeconfig provided")
5354
self.kubeconfig_path = kubeconfig
5455
self.working_directory = os.getcwd()
56+
if result_dir_name is None:
57+
result_dir_name = os.path.join(gettempdir(), check_name)
5558
self.result_dir_name = result_dir_name
5659
self.sonobuoy = _find_sonobuoy()
5760
logger.debug(f"working from {self.working_directory}")
@@ -118,12 +121,8 @@ def run(self):
118121
self._sonobuoy_run()
119122
counter = self._sonobuoy_retrieve_result()
120123
return_code = self._eval_result(counter)
121-
print(self.check_name + ": " + ("PASS", "FAIL")[min(1, return_code)])
124+
logger.debug(self.check_name + ": " + ("PASS", "FAIL")[min(1, return_code)])
122125
return return_code
123-
except BaseException:
124-
print(self.check_name + ": ABORT")
125-
logger.critical("something went wrong", exc_info=True)
126-
return 112
127126
finally:
128127
self._sonobuoy_delete()
129128

Tests/scs-compatible-kaas.yaml

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,19 @@ variables:
1111
- kubeconfig
1212
- execution_mode: parallel # alternatively: serial, dry
1313
scripts:
14-
- executable: ./kaas/sonobuoy_handler/run_sonobuoy.py
15-
args: >-
16-
run -k {kubeconfig}
17-
--scs-sonobuoy-config kaas/scs-sonobuoy-config-v1.yaml
18-
--execution-mode {execution_mode}
19-
-r {subject_root}/sono-results-e2e
20-
-c 'cncf-k8s-conformance'
21-
-a '--mode=certified-conformance'
22-
-a '--plugin-env e2e.E2E_EXTRA_ARGS=--ginkgo.flake-attempts=2'
14+
- executable: ./kaas/k8s_test.py
15+
args: -k {kubeconfig} --execution-mode {execution_mode} {testcases}
2316
testcases:
2417
- id: cncf-k8s-conformance
2518
lifetime: year
2619
section: heavy
2720
description: Must fulfill all requirements of scs-0201-v1 (CNCF Kubernetes conformance).
2821
url: https://docs.scs.community/standards/scs-0201-v1-cncf-conformance#regulations
29-
- executable: ./kaas/scs_0210_version_policy/k8s_version_policy.py
30-
args: -k {kubeconfig}
31-
testcases:
22+
- id: kaas-networking-check
23+
lifetime: month
24+
section: heavy
25+
description: Must pass all testcases of upstream CNCF Kubernetes e2e-suite focus 'NetworkPolicy'.
26+
url: https://docs.scs.community/standards/scs-0219-w1-kaas-networking#automated-tests
3227
- id: version-policy-check
3328
section: light
3429
description: Must fulfill all requirements of scs-0210-v2.
@@ -38,21 +33,6 @@ scripts:
3833
description: >-
3934
Manual check: Must fulfill all requirements of scs-0214-v2.
4035
url: https://docs.scs.community/standards/scs-0214-v2-k8s-node-distribution#decision
41-
- executable: ./kaas/sonobuoy_handler/run_sonobuoy.py
42-
args: >-
43-
run -k {kubeconfig}
44-
--scs-sonobuoy-config kaas/scs-sonobuoy-config-v1.yaml
45-
--execution-mode {execution_mode}
46-
-r {subject_root}/sono-results-0219
47-
-c 'kaas-networking-check'
48-
-a '--e2e-focus "NetworkPolicy"'
49-
-a '--plugin-env e2e.E2E_EXTRA_ARGS=--ginkgo.flake-attempts=2'
50-
testcases:
51-
- id: kaas-networking-check
52-
lifetime: month
53-
section: heavy
54-
description: Must pass all testcases of upstream CNCF Kubernetes e2e-suite focus 'NetworkPolicy'.
55-
url: https://docs.scs.community/standards/scs-0219-w1-kaas-networking#automated-tests
5636
groups:
5737
- id: scs-0201-v1
5838
name: CNCF Kubernetes conformance

0 commit comments

Comments
 (0)