Skip to content

Commit 8231dc1

Browse files
committed
Inital sonobuoy integration
The scripts introduced in this commit allow using Sonobuoy to: - Run kubernetes e2e tests - Parse results from the sonobouy testsuite Signed-off-by: Toni Finger <[email protected]>
1 parent a96047d commit 8231dc1

File tree

3 files changed

+259
-5
lines changed

3 files changed

+259
-5
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/usr/bin/env python3
2+
# vim: set ts=4 sw=4 et:
3+
#
4+
5+
from sonobuoy_handler import SonobuoyHandler
6+
import sys
7+
import click
8+
import logging
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
@click.command()
14+
@click.option("-k", "--kubeconfig", "kubeconfig", type=click.Path(exists=False), default=None, help="path/to/kubeconfig_file.yaml",)
15+
@click.option("-r", "--result_dir_name", "result_dir_name", type=str, default="sonobuoy_results", help="directory name to store results at",)
16+
@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'",)
17+
@click.option("-a", "--arg", "args", multiple=True)
18+
def sonobuoy_run(kubeconfig, result_dir_name, check_name, args):
19+
logger.info("Run sonobuoy_executor")
20+
sonobuoy_handler = SonobuoyHandler(check_name, kubeconfig, result_dir_name, args)
21+
return_code = sonobuoy_handler.run()
22+
sys.exit(return_code)
23+
24+
25+
if __name__ == "__main__":
26+
sonobuoy_run()
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
from kubernetes import client, config
2+
import os
3+
import subprocess
4+
import logging
5+
from junitparser import JUnitXml
6+
import json
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
def setup_k8s_client(kubeconfigfile=None):
12+
13+
if kubeconfigfile:
14+
logger.debug(f"loading kubeconfig file '{kubeconfigfile}'")
15+
config.load_kube_config(kubeconfigfile)
16+
logger.info("kubeconfigfile loaded successfully")
17+
else:
18+
logger.error("no kubeconfig file provided")
19+
return None
20+
k8s_api_client = client.CoreV1Api()
21+
return k8s_api_client
22+
23+
24+
class SonobuoyHandler:
25+
"""
26+
A class that handles both the execution of sonobuoy and
27+
the generation of the results for a test report
28+
"""
29+
30+
kubeconfig_path = None
31+
working_directory = None
32+
33+
def __init__(
34+
self,
35+
check_name="sonobuoy_handler",
36+
kubeconfig=None,
37+
result_dir_name="sonobuoy_results",
38+
args=None,
39+
):
40+
self.check_name = check_name
41+
logger.info(f"Inital {__name__} for {self.check_name}")
42+
logger.debug(f"kubeconfig: {kubeconfig} ")
43+
if kubeconfig is None:
44+
raise Exception("No kubeconfig provided")
45+
else:
46+
self.kubeconfig_path = kubeconfig
47+
self.working_directory = os.getcwd()
48+
self.result_dir_name = result_dir_name
49+
logger.debug(
50+
f"Working from {self.working_directory} placing results at {self.result_dir_name}"
51+
)
52+
self.args = args
53+
54+
def _build_command(self, process, args):
55+
command = (
56+
[
57+
"sonobuoy",
58+
"--kubeconfig",
59+
self.kubeconfig_path,
60+
] + [process] + args
61+
)
62+
command_string = ""
63+
for entry in command:
64+
command_string += entry + " "
65+
return command_string
66+
67+
def _sonobuoy_run(self):
68+
logger.debug(f"sonobuoy run")
69+
check_args = ["--wait"]
70+
check_args += [str(arg) for arg in self.args]
71+
subprocess.run(
72+
self._build_command("run", check_args),
73+
shell=True,
74+
capture_output=True,
75+
check=True,
76+
)
77+
78+
def _sonobouy_delete(self):
79+
logger.info("removing sonobuoy resources from cluster")
80+
subprocess.run(
81+
self._build_command("delete", ["--wait"]),
82+
shell=True,
83+
capture_output=True,
84+
check=True,
85+
)
86+
87+
def _sonobouy_status_result(self):
88+
logger.debug(f"sonobuoy status")
89+
process = subprocess.run(
90+
self._build_command("status", ["--json"]),
91+
shell=True,
92+
capture_output=True,
93+
check=True,
94+
)
95+
json_data = json.loads(process.stdout)
96+
for entry in json_data["plugins"]:
97+
print(f"plugin:{entry['plugin']}:{entry['result-status']}")
98+
failed_test_cases = 0
99+
passed_test_cases = 0
100+
skipped_test_cases = 0
101+
for result, count in json_data["plugins"][0]["result-counts"].items():
102+
if result == "passed":
103+
passed_test_cases = count
104+
if result == "failed":
105+
failed_test_cases = count
106+
logger.error(f"ERROR: failed: {count}")
107+
if result == "skipped":
108+
skipped_test_cases = count
109+
logger.error(f"ERROR: skipped: {count}")
110+
result_message = f" {passed_test_cases} passed, {failed_test_cases} failed, {skipped_test_cases} skipped"
111+
if failed_test_cases == 0 and skipped_test_cases == 0:
112+
logger.info(result_message)
113+
self.return_code = 0
114+
else:
115+
logger.error("ERROR:" + result_message)
116+
self.return_code = 3
117+
118+
def _preflight_check(self):
119+
"""
120+
Prefligth test to ensure that everything is set up correctly for execution
121+
:param: None
122+
:return: None
123+
"""
124+
logger.info("check kubeconfig")
125+
print("check kubeconfig")
126+
self.k8s_api_client = setup_k8s_client(self.kubeconfig_path)
127+
128+
for api in client.ApisApi().get_api_versions().groups:
129+
versions = []
130+
for v in api.versions:
131+
name = ""
132+
if v.version == api.preferred_version.version and len(api.versions) > 1:
133+
name += "*"
134+
name += v.version
135+
versions.append(name)
136+
logger.info(f"[supported api]: {api.name:<40} {','.join(versions)}")
137+
138+
logger.debug("checks if sonobuoy is availabe")
139+
return_value = os.system(
140+
f"sonobuoy version --kubeconfig='{self.kubeconfig_path}'"
141+
)
142+
if return_value != 0:
143+
raise Exception("sonobuoy is not installed")
144+
145+
def _sonobuoy_retrieve_result(self):
146+
"""
147+
This method invokes sonobouy to store the results in a subdirectory of
148+
the working directory. The Junit results file contained in it is then
149+
analyzed in order to interpret the relevant information it containes
150+
:param: result_file_name:
151+
:return: None
152+
"""
153+
logger.debug(f"retrieving results to {self.result_dir_name}")
154+
print(f"retrieving results to {self.result_dir_name}")
155+
result_dir = self.working_directory + "/" + self.result_dir_name
156+
if os.path.exists(result_dir):
157+
raise Exception("result directory allready excisting")
158+
else:
159+
os.mkdir(result_dir)
160+
161+
os.system(
162+
# ~ f"sonobuoy retrieve {result_dir} -x --filename='{result_dir}' --kubeconfig='{self.kubeconfig_path}'"
163+
f"sonobuoy retrieve {result_dir} --kubeconfig='{self.kubeconfig_path}'"
164+
)
165+
logger.debug(
166+
f"parsing JUnit result from {result_dir + '/plugins/e2e/results/global/junit_01.xml'} "
167+
)
168+
xml = JUnitXml.fromfile(result_dir + "/plugins/e2e/results/global/junit_01.xml")
169+
failed_test_cases = 0
170+
passed_test_cases = 0
171+
skipped_test_cases = 0
172+
for suite in xml:
173+
for case in suite:
174+
if case.is_passed is True:
175+
passed_test_cases += 1
176+
elif case.is_skipped is True:
177+
skipped_test_cases += 1
178+
# ~ logger.warning(f"SKIPPED:{case.name}") # TODO:!!! decide if skipped is error or warning only ?
179+
else:
180+
failed_test_cases += 1
181+
logger.error(f"ERROR: {case.name}")
182+
print(f"ERROR: {case.name}")
183+
184+
result_message = f" {passed_test_cases} passed, {failed_test_cases} failed, {skipped_test_cases} skipped"
185+
if failed_test_cases == 0 and skipped_test_cases == 0:
186+
logger.info(result_message)
187+
self.return_code = 0
188+
else:
189+
logger.error("ERROR:" + result_message)
190+
self.return_code = 3
191+
192+
def run(self):
193+
"""
194+
This method is to be called to run the plugin
195+
"""
196+
self.return_code = 11
197+
self._preflight_check()
198+
self._sonobuoy_run()
199+
self._sonobouy_status_result()
200+
201+
# ERROR: currently disabled do to: "error retrieving results: unexpected EOF"
202+
# migth be related to following bug: https://github.com/vmware-tanzu/sonobuoy/issues/1633
203+
# self._sonobuoy_retrieve_result(self)
204+
205+
self._sonobouy_delete()
206+
print(self.check_name + ": " + ("PASS", "FAIL")[min(1, self.return_code)])
207+
return self.return_code

Tests/scs-compatible-kaas.yaml

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,35 @@ url: https://raw.githubusercontent.com/SovereignCloudStack/standards/main/Tests/
44
variables:
55
- subject_root # directory containing the kubeconfig files for the subject under test
66
modules:
7+
8+
9+
710
- id: cncf-k8s-conformance
811
name: CNCF Kubernetes conformance
912
url: https://github.com/cncf/k8s-conformance/tree/master
10-
#run:
11-
# - executable: ./kaas/plugin/run_sonobuoy_executor.py
12-
# args: -k {subject_root}/kubeconfig.yaml -r {result_dir_path} -c 'cncf-conformance'
13+
run:
14+
- executable: ./kaas/sonobuoy_handler/run_sonobuoy.py
15+
#~ args: -k {subject_root}/kubeconfig.yaml -r {subject_root}/sono-results -c 'cncf-conformance' -a '--mode=certified-conformance'
16+
args: -k {subject_root}/kubeconfig.yaml -r {subject_root}/sono-results -c 'cncf-k8s-conformance' -a '--plugin-env e2e.E2E_DRYRUN=true'
1317
testcases:
1418
- id: cncf-k8s-conformance
1519
tags: [mandatory]
20+
21+
22+
23+
#~ - id: scs-XXXX-v1
24+
#~ name: Kubernetes network policy
25+
#~ url: https://raw.githubusercontent.com/SovereignCloudStack/standards/main/Standards/scs-XXXX-v1-kaas-networking.md
26+
#parameters:
27+
#~  #mode: sonobouy mode to run
28+
#~ run:
29+
#~ - executable: ./kaas/sonobuoy_handler/run_sonobuoy.py
30+
#~ args: -k {subject_root}/kubeconfig.yaml -r {subject_root}/sono-results -c 'cncf-debug' -a '--e2e-focus "NetworkPolicy"'
31+
#~ testcases:
32+
#~ - id: cncf-k8s-conformance
33+
#~ tags: [mandatory]
34+
35+
1636
- id: scs-0210-v2
1737
name: Kubernetes version policy
1838
url: https://raw.githubusercontent.com/SovereignCloudStack/standards/main/Standards/scs-0210-v2-k8s-version-policy.md
@@ -40,7 +60,8 @@ versions:
4060
- version: v1
4161
include:
4262
- ref: cncf-k8s-conformance
43-
- ref: scs-0210-v2
44-
- ref: scs-0214-v2
63+
#~ - ref: scs-XXXX-v1
64+
#~ - ref: scs-0210-v2
65+
#~ - ref: scs-0214-v2
4566
targets:
4667
main: mandatory

0 commit comments

Comments
 (0)