Skip to content

Commit f29b4c4

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

4 files changed

Lines changed: 138 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: 91 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from collections import Counter
2-
import json
32
import logging
3+
import re
44
import os
55
import os.path
66
import shlex
@@ -25,7 +25,7 @@ def _find_sonobuoy():
2525

2626

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

3030

3131
class SonobuoyHandler:
@@ -42,6 +42,7 @@ def __init__(
4242
check_name="sonobuoy_handler",
4343
kubeconfig=None,
4444
result_dir_name="sonobuoy_results",
45+
scs_sonobuoy_config_yaml="kaas/sonobuoy-config.yaml",
4546
args=(),
4647
):
4748
self.check_name = check_name
@@ -55,6 +56,9 @@ def __init__(
5556
logger.debug(f"working from {self.working_directory}")
5657
logger.debug(f"placing results at {self.result_dir_name}")
5758
logger.debug(f"sonobuoy executable at {self.sonobuoy}")
59+
if not os.path.exists(scs_sonobuoy_config_yaml):
60+
raise RuntimeError(f"scs_sonobuoy_config_yaml {scs_sonobuoy_config_yaml} does not exist.")
61+
self.scs_sonobuoy_config_yaml = scs_sonobuoy_config_yaml
5862
self.args = (arg0 for arg in args for arg0 in shlex.split(str(arg)))
5963

6064
def _invoke_sonobuoy(self, *args, **kwargs):
@@ -69,16 +73,6 @@ def _sonobuoy_run(self):
6973
def _sonobuoy_delete(self):
7074
self._invoke_sonobuoy("delete", "--wait")
7175

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-
8276
def _eval_result(self, counter):
8377
"""evaluate test results and return return code"""
8478
result_message = f"sonobuoy reports {_fmt_result(counter)}"
@@ -99,7 +93,7 @@ def _sonobuoy_retrieve_result(self, plugin='e2e'):
9993
"""
10094
Invoke sonobuoy to retrieve results and to store them in a subdirectory of
10195
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`.
96+
log each failure as ERROR. Return summary dict.
10397
"""
10498
logger.debug(f"retrieving results to {self.result_dir_name}")
10599
result_dir = os.path.join(self.working_directory, self.result_dir_name)
@@ -108,21 +102,9 @@ def _sonobuoy_retrieve_result(self, plugin='e2e'):
108102
self._invoke_sonobuoy("retrieve", "-x", result_dir)
109103
yaml_path = os.path.join(result_dir, 'plugins', plugin, 'sonobuoy_results.yaml')
110104
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
105+
ok_to_fail_regex_list = _load_ok_to_fail_regex_list(self.scs_sonobuoy_config_yaml)
106+
107+
return sonobuoy_parse_result(plugin, yaml_path, ok_to_fail_regex_list)
126108

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