Skip to content

Commit 4e19052

Browse files
committed
Allow some Sonobuoy tests (KaaS) to fail.
Signed-off-by: Thomas Güttler <thomas.guettler@syself.com>
1 parent af2c1f7 commit 4e19052

4 files changed

Lines changed: 139 additions & 41 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
okToFail:
2+
# InternalIP still used in tests. Having only an ExternalIP is considered valid by SCS:
3+
- regex: HostPort validates that there is no conflict between pods with same hostPort but different hostIP and protocol
4+
reason: "Fails when a cluster has no InternalIP (only ExternalIP): https://github.com/kubernetes/kubernetes/issues/136626"
5+
- regex: Services should be able to switch session affinity for NodePort service
6+
reason: "Fails when a cluster has no InternalIP (only ExternalIP): https://github.com/kubernetes/kubernetes/issues/136626"
7+
- regex: Services should have session affinity work for NodePort service
8+
reason: "Fails when a cluster has no InternalIP (only ExternalIP): https://github.com/kubernetes/kubernetes/issues/136626"
9+
- regex: validates that there exists conflict between pods with same hostPort and protocol but one using 0.0.0.0 hostIP
10+
reason: "Fails when a cluster has no InternalIP (only ExternalIP): https://github.com/kubernetes/kubernetes/issues/136626"
11+
12+
# Was flaky
13+
- regex: Netpol NetworkPolicy between server and client should allow ingress access from updated namespace
14+
reason: Flaky test. Fix in v1.36 (https://github.com/kubernetes/kubernetes/pull/136715)
15+
16+
# SCTP is optional
17+
- regex: Feature:SCTPConnectivity
18+
reason: SCTPConnectivity is optional. Currently Cilium does not support it by default.
19+
20+
# Tests skipped by Cilium: https://github.com/cilium/cilium/blob/main/.github/workflows/k8s-kind-network-policies-e2e.yaml#L177-L185
21+
- regex: should.allow.egress.access.to.server.in.CIDR.block
22+
reason: https://github.com/cilium/cilium/issues/9209
23+
- regex: should.ensure.an.IP.overlapping.both.IPBlock.CIDR.and.IPBlock.Except.is.allowed
24+
reason: https://github.com/cilium/cilium/issues/9209
25+
- regex: should.enforce.except.clause.while.egress.access.to.server.in.CIDR.block
26+
reason: https://github.com/cilium/cilium/issues/9209

Tests/kaas/sonobuoy_handler/run_sonobuoy.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,35 @@
66

77
import click
88

9-
from sonobuoy_handler import SonobuoyHandler
9+
from sonobuoy_handler import SonobuoyHandler, check_sonobuoy_result
1010

1111
logger = logging.getLogger(__name__)
1212

1313

14-
@click.command()
14+
@click.group()
15+
def cli():
16+
pass
17+
18+
19+
@cli.command("run")
1520
@click.option("-k", "--kubeconfig", "kubeconfig", required=True, type=click.Path(exists=True), help="path/to/kubeconfig_file.yaml",)
1621
@click.option("-r", "--result_dir_name", "result_dir_name", type=str, default="sonobuoy_results", help="directory name to store results at",)
1722
@click.option("-c", "--check", "check_name", type=str, default="sonobuoy_executor", help="this MUST be the same name as the id in 'scs-compatible-kaas.yaml'",)
23+
@click.option("--scs-sonobuoy-config", "scs_sonobuoy_config_yaml", type=click.Path(exists=True), default="kaas/sonobuoy-config.yaml", help="Configuration for Sonobuoy (yaml format)")
1824
@click.option("-a", "--arg", "args", multiple=True)
19-
def sonobuoy_run(kubeconfig, result_dir_name, check_name, args):
20-
sonobuoy_handler = SonobuoyHandler(check_name, kubeconfig, result_dir_name, args)
25+
def sonobuoy_run(kubeconfig, result_dir_name, check_name, scs_sonobuoy_config_yaml, args):
26+
sonobuoy_handler = SonobuoyHandler(check_name, kubeconfig, result_dir_name, scs_sonobuoy_config_yaml, args)
2127
sys.exit(sonobuoy_handler.run())
2228

2329

30+
@cli.command("check-results")
31+
@click.option("--scs-sonobuoy-config", "scs_sonobuoy_config_yaml", type=click.Path(exists=True), default="kaas/scs-sonobuoy-config.yaml", help="Configuration for Sonobuoy (yaml format)")
32+
@click.argument("sonobuoy_result_yaml", type=click.Path(exists=True))
33+
def check_results(scs_sonobuoy_config_yaml, sonobuoy_result_yaml):
34+
check_sonobuoy_result(scs_sonobuoy_config_yaml, sonobuoy_result_yaml)
35+
sys.exit(0)
36+
37+
2438
if __name__ == "__main__":
2539
logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG)
26-
sonobuoy_run()
40+
cli()

Tests/kaas/sonobuoy_handler/sonobuoy_handler.py

Lines changed: 92 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from collections import Counter
2-
import json
32
import logging
3+
import re
44
import os
5+
import sys
56
import os.path
67
import shlex
78
import shutil
@@ -25,7 +26,7 @@ def _find_sonobuoy():
2526

2627

2728
def _fmt_result(counter):
28-
return ', '.join(f"{counter.get(key, 0)} {key}" for key in ('passed', 'failed', 'skipped'))
29+
return ', '.join(f"{counter.get(key, 0)} {key}" for key in ('passed', 'failed', 'failed_ok', 'skipped'))
2930

3031

3132
class SonobuoyHandler:
@@ -42,6 +43,7 @@ def __init__(
4243
check_name="sonobuoy_handler",
4344
kubeconfig=None,
4445
result_dir_name="sonobuoy_results",
46+
scs_sonobuoy_config_yaml="kaas/sonobuoy-config.yaml",
4547
args=(),
4648
):
4749
self.check_name = check_name
@@ -55,6 +57,9 @@ def __init__(
5557
logger.debug(f"working from {self.working_directory}")
5658
logger.debug(f"placing results at {self.result_dir_name}")
5759
logger.debug(f"sonobuoy executable at {self.sonobuoy}")
60+
if not os.path.exists(scs_sonobuoy_config_yaml):
61+
raise RuntimeError(f"scs_sonobuoy_config_yaml {scs_sonobuoy_config_yaml} does not exist.")
62+
self.scs_sonobuoy_config_yaml = scs_sonobuoy_config_yaml
5863
self.args = (arg0 for arg in args for arg0 in shlex.split(str(arg)))
5964

6065
def _invoke_sonobuoy(self, *args, **kwargs):
@@ -69,16 +74,6 @@ def _sonobuoy_run(self):
6974
def _sonobuoy_delete(self):
7075
self._invoke_sonobuoy("delete", "--wait")
7176

72-
def _sonobuoy_status_result(self):
73-
process = self._invoke_sonobuoy("status", "--json", capture_output=True)
74-
json_data = json.loads(process.stdout)
75-
counter = Counter()
76-
for entry in json_data["plugins"]:
77-
logger.debug(f"plugin {entry['plugin']}: {_fmt_result(entry['result-counts'])}")
78-
for key, value in entry["result-counts"].items():
79-
counter[key] += value
80-
return counter
81-
8277
def _eval_result(self, counter):
8378
"""evaluate test results and return return code"""
8479
result_message = f"sonobuoy reports {_fmt_result(counter)}"
@@ -99,7 +94,7 @@ def _sonobuoy_retrieve_result(self, plugin='e2e'):
9994
"""
10095
Invoke sonobuoy to retrieve results and to store them in a subdirectory of
10196
the working directory. Analyze the results yaml file for given `plugin` and
102-
log each failure as ERROR. Return summary dict like `_sonobuoy_status_result`.
97+
log each failure as ERROR. Return summary dict.
10398
"""
10499
logger.debug(f"retrieving results to {self.result_dir_name}")
105100
result_dir = os.path.join(self.working_directory, self.result_dir_name)
@@ -108,21 +103,9 @@ def _sonobuoy_retrieve_result(self, plugin='e2e'):
108103
self._invoke_sonobuoy("retrieve", "-x", result_dir)
109104
yaml_path = os.path.join(result_dir, 'plugins', plugin, 'sonobuoy_results.yaml')
110105
logger.debug(f"parsing results from {yaml_path}")
111-
with open(yaml_path, "r") as fileobj:
112-
result_obj = yaml.load(fileobj.read(), yaml.SafeLoader)
113-
counter = Counter()
114-
for item1 in result_obj.get('items', ()):
115-
# file ...
116-
for item2 in item1.get('items', ()):
117-
# suite ...
118-
for item in item2.get('items', ()):
119-
# testcase ... or so
120-
status = item.get('status', 'skipped')
121-
counter[status] += 1
122-
if status == 'failed':
123-
logger.error(f"FAILED: {item['name']}") # <-- this is why this method exists!
124-
logger.info(f"{plugin} results: {_fmt_result(counter)}")
125-
return counter
106+
ok_to_fail_regex_list = _load_ok_to_fail_regex_list(self.scs_sonobuoy_config_yaml)
107+
108+
return sonobuoy_parse_result(plugin, yaml_path, ok_to_fail_regex_list)
126109

127110
def run(self):
128111
"""
@@ -132,16 +115,91 @@ def run(self):
132115
self._preflight_check()
133116
try:
134117
self._sonobuoy_run()
135-
return_code = self._eval_result(self._sonobuoy_status_result())
118+
counter = self._sonobuoy_retrieve_result()
119+
return_code = self._eval_result(counter)
136120
print(self.check_name + ": " + ("PASS", "FAIL")[min(1, return_code)])
137-
try:
138-
self._sonobuoy_retrieve_result()
139-
except Exception:
140-
# swallow exception for the time being
141-
logger.debug('problem retrieving results', exc_info=True)
142121
return return_code
143122
except BaseException:
144123
logger.exception("something went wrong")
145124
return 112
146125
finally:
147126
self._sonobuoy_delete()
127+
128+
129+
def sonobuoy_parse_result(plugin, sonobuoy_results_yaml_path, ok_to_fail_regex_list):
130+
with open(sonobuoy_results_yaml_path, "r") as fileobj:
131+
result_obj = yaml.load(fileobj.read(), yaml.SafeLoader)
132+
133+
counter = Counter()
134+
for item1 in result_obj.get("items", ()):
135+
# file ...
136+
for item2 in item1.get("items", ()):
137+
# suite ...
138+
for item in item2.get("items", ()):
139+
# testcase ... or so
140+
status = item.get("status", "skipped")
141+
if status == "failed":
142+
if ok_to_fail(ok_to_fail_regex_list, item["name"]):
143+
status = "failed_ok"
144+
else:
145+
logger.error(f"FAILED: {item['name']}")
146+
counter[status] += 1
147+
148+
logger.info(f"{plugin} results: {_fmt_result(counter)}")
149+
return counter
150+
151+
152+
def _load_ok_to_fail_regex_list(scs_sonobuoy_config_yaml):
153+
with open(scs_sonobuoy_config_yaml, "r") as fileobj:
154+
config_obj = yaml.load(fileobj.read(), yaml.SafeLoader) or {}
155+
if not isinstance(config_obj, dict):
156+
raise ValueError(f"Invalid sonobuoy config format in {scs_sonobuoy_config_yaml}: top-level YAML object must be a mapping")
157+
allowed_top_level_keys = {"okToFail"}
158+
unknown_top_level_keys = set(config_obj) - allowed_top_level_keys
159+
if unknown_top_level_keys:
160+
raise ValueError(
161+
f"Invalid sonobuoy config format in {scs_sonobuoy_config_yaml}: unknown top-level keys: {sorted(unknown_top_level_keys)}"
162+
)
163+
ok_to_fail_items = config_obj.get("okToFail", ())
164+
if not isinstance(ok_to_fail_items, list):
165+
raise ValueError(f"Invalid sonobuoy config format in {scs_sonobuoy_config_yaml}: okToFail must be a list")
166+
167+
ok_to_fail_regex_list = []
168+
for idx, entry in enumerate(ok_to_fail_items):
169+
if not isinstance(entry, dict):
170+
raise ValueError(
171+
f"Invalid sonobuoy config format in {scs_sonobuoy_config_yaml}: okToFail[{idx}] must be a mapping"
172+
)
173+
allowed_entry_keys = {"regex", "reason"}
174+
unknown_entry_keys = set(entry) - allowed_entry_keys
175+
if unknown_entry_keys:
176+
raise ValueError(
177+
f"Invalid sonobuoy config format in {scs_sonobuoy_config_yaml}: okToFail[{idx}] has unknown keys: {sorted(unknown_entry_keys)}"
178+
)
179+
regex = entry.get("regex")
180+
if not isinstance(regex, str) or not regex.strip():
181+
raise ValueError(
182+
f"Invalid sonobuoy config format in {scs_sonobuoy_config_yaml}: okToFail[{idx}].regex must be a non-empty string"
183+
)
184+
reason = entry.get("reason")
185+
if not isinstance(reason, str) or not reason.strip():
186+
raise ValueError(
187+
f"Invalid sonobuoy config format in {scs_sonobuoy_config_yaml}: okToFail[{idx}].reason must be a non-empty string"
188+
)
189+
ok_to_fail_regex_list.append((re.compile(regex), reason))
190+
return ok_to_fail_regex_list
191+
192+
193+
def ok_to_fail(ok_to_fail_regex_list, test_name):
194+
name = test_name
195+
for regex, _ in ok_to_fail_regex_list:
196+
if re.search(regex, name):
197+
return True
198+
return False
199+
200+
201+
def check_sonobuoy_result(scs_sonobuoy_config_yaml, result_yaml):
202+
ok_to_fail_regex_list = _load_ok_to_fail_regex_list(scs_sonobuoy_config_yaml)
203+
counter = sonobuoy_parse_result("", result_yaml, ok_to_fail_regex_list)
204+
for key, value in counter.items():
205+
print(f"{key}: {value}")

Tests/scs-compatible-kaas.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ variables:
1111
- kubeconfig
1212
scripts:
1313
- executable: ./kaas/sonobuoy_handler/run_sonobuoy.py
14-
args: -k {kubeconfig} -r {subject_root}/sono-results-e2e -c 'cncf-k8s-conformance' -a '--mode=certified-conformance'
14+
args: run -k {kubeconfig} --scs-sonobuoy-config kaas/scs-sonobuoy-config.yaml -r {subject_root}/sono-results-e2e -c 'cncf-k8s-conformance' -a '--mode=certified-conformance'
1515
#~ args: -k {kubeconfig} -r {subject_root}/sono-results -c 'cncf-k8s-conformance' -a '--plugin-env e2e.E2E_DRYRUN=true'
1616
testcases:
1717
- id: cncf-k8s-conformance
@@ -31,7 +31,7 @@ scripts:
3131
description: Must fulfill all requirements of scs-0214-v2.
3232
url: https://docs.scs.community/standards/scs-0214-v2-k8s-node-distribution#decision
3333
- executable: ./kaas/sonobuoy_handler/run_sonobuoy.py
34-
args: -k {kubeconfig} -r {subject_root}/sono-results-0219 -c 'kaas-networking-check' -a '--e2e-focus "NetworkPolicy"'
34+
args: run -k {kubeconfig} --scs-sonobuoy-config kaas/scs-sonobuoy-config.yaml -r {subject_root}/sono-results-0219 -c 'kaas-networking-check' -a '--e2e-focus "NetworkPolicy"'
3535
testcases:
3636
- id: kaas-networking-check
3737
description: Must fulfill all requirements of scs-0219-v1.

0 commit comments

Comments
 (0)