|
| 1 | +from collections import Counter |
| 2 | +import json |
| 3 | +import logging |
| 4 | +import os |
| 5 | +import shlex |
| 6 | +import shutil |
| 7 | +import subprocess |
| 8 | + |
| 9 | +from junitparser import JUnitXml |
| 10 | + |
| 11 | +logger = logging.getLogger(__name__) |
| 12 | + |
| 13 | + |
| 14 | +class SonobuoyHandler: |
| 15 | + """ |
| 16 | + A class that handles both the execution of sonobuoy and |
| 17 | + the generation of the results for a test report |
| 18 | + """ |
| 19 | + |
| 20 | + kubeconfig_path = None |
| 21 | + working_directory = None |
| 22 | + |
| 23 | + def __init__( |
| 24 | + self, |
| 25 | + check_name="sonobuoy_handler", |
| 26 | + kubeconfig=None, |
| 27 | + result_dir_name="sonobuoy_results", |
| 28 | + args=(), |
| 29 | + ): |
| 30 | + self.check_name = check_name |
| 31 | + logger.debug(f"kubeconfig: {kubeconfig} ") |
| 32 | + if kubeconfig is None: |
| 33 | + raise RuntimeError("No kubeconfig provided") |
| 34 | + self.kubeconfig_path = kubeconfig |
| 35 | + self.working_directory = os.getcwd() |
| 36 | + self.result_dir_name = result_dir_name |
| 37 | + self.sonobuoy = shutil.which('sonobuoy') |
| 38 | + logger.debug(f"working from {self.working_directory}") |
| 39 | + logger.debug(f"placing results at {self.result_dir_name}") |
| 40 | + logger.debug(f"sonobuoy executable at {self.sonobuoy}") |
| 41 | + self.args = (arg0 for arg in args for arg0 in shlex.split(str(arg))) |
| 42 | + |
| 43 | + def _invoke_sonobuoy(self, *args, **kwargs): |
| 44 | + inv_args = (self.sonobuoy, "--kubeconfig", self.kubeconfig_path) + args |
| 45 | + logger.debug(f'invoking {" ".join(inv_args)}') |
| 46 | + return subprocess.run(args=inv_args, capture_output=True, check=True, **kwargs) |
| 47 | + |
| 48 | + def _sonobuoy_run(self): |
| 49 | + self._invoke_sonobuoy("run", "--wait", *self.args) |
| 50 | + |
| 51 | + def _sonobuoy_delete(self): |
| 52 | + self._invoke_sonobuoy("delete", "--wait") |
| 53 | + |
| 54 | + def _sonobuoy_status_result(self): |
| 55 | + process = self._invoke_sonobuoy("status", "--json") |
| 56 | + json_data = json.loads(process.stdout) |
| 57 | + counter = Counter() |
| 58 | + for entry in json_data["plugins"]: |
| 59 | + logger.debug(f"plugin:{entry['plugin']}:{entry['result-status']}") |
| 60 | + for result, count in entry["result-counts"].items(): |
| 61 | + counter[result] += count |
| 62 | + return counter |
| 63 | + |
| 64 | + def _eval_result(self, counter): |
| 65 | + """evaluate test results and return return code""" |
| 66 | + result_str = ', '.join(f"{counter[key]} {key}" for key in ('passed', 'failed', 'skipped')) |
| 67 | + result_message = f"sonobuoy reports {result_str}" |
| 68 | + if counter['failed']: |
| 69 | + logger.error(result_message) |
| 70 | + return 3 |
| 71 | + logger.info(result_message) |
| 72 | + return 0 |
| 73 | + |
| 74 | + def _preflight_check(self): |
| 75 | + """ |
| 76 | + Preflight test to ensure that everything is set up correctly for execution |
| 77 | + """ |
| 78 | + if not self.sonobuoy: |
| 79 | + raise RuntimeError("sonobuoy executable not found; is it in PATH?") |
| 80 | + |
| 81 | + def _sonobuoy_retrieve_result(self): |
| 82 | + """ |
| 83 | + This method invokes sonobuoy to store the results in a subdirectory of |
| 84 | + the working directory. The Junit results file contained in it is then |
| 85 | + analyzed in order to interpret the relevant information it containes |
| 86 | + """ |
| 87 | + logger.debug(f"retrieving results to {self.result_dir_name}") |
| 88 | + result_dir = os.path.join(self.working_directory, self.result_dir_name) |
| 89 | + if os.path.exists(result_dir): |
| 90 | + raise Exception("result directory already existing") |
| 91 | + os.mkdir(result_dir) |
| 92 | + |
| 93 | + # XXX use self._invoke_sonobuoy |
| 94 | + os.system( |
| 95 | + # ~ f"sonobuoy retrieve {result_dir} -x --filename='{result_dir}' --kubeconfig='{self.kubeconfig_path}'" |
| 96 | + f"sonobuoy retrieve {result_dir} --kubeconfig='{self.kubeconfig_path}'" |
| 97 | + ) |
| 98 | + logger.debug( |
| 99 | + f"parsing JUnit result from {result_dir + '/plugins/e2e/results/global/junit_01.xml'} " |
| 100 | + ) |
| 101 | + xml = JUnitXml.fromfile(result_dir + "/plugins/e2e/results/global/junit_01.xml") |
| 102 | + counter = Counter() |
| 103 | + for suite in xml: |
| 104 | + for case in suite: |
| 105 | + if case.is_passed is True: # XXX why `is True`??? |
| 106 | + counter['passed'] += 1 |
| 107 | + elif case.is_skipped is True: |
| 108 | + counter['skipped'] += 1 |
| 109 | + else: |
| 110 | + counter['failed'] += 1 |
| 111 | + logger.error(f"{case.name}") |
| 112 | + return counter |
| 113 | + |
| 114 | + def run(self): |
| 115 | + """ |
| 116 | + This method is to be called to run the plugin |
| 117 | + """ |
| 118 | + logger.info(f"running sonobuoy for testcase {self.check_name}") |
| 119 | + self._preflight_check() |
| 120 | + try: |
| 121 | + self._sonobuoy_run() |
| 122 | + return_code = self._eval_result(self._sonobuoy_status_result()) |
| 123 | + print(self.check_name + ": " + ("PASS", "FAIL")[min(1, return_code)]) |
| 124 | + return return_code |
| 125 | + |
| 126 | + # ERROR: currently disabled due to: "error retrieving results: unexpected EOF" |
| 127 | + # might be related to following bug: https://github.com/vmware-tanzu/sonobuoy/issues/1633 |
| 128 | + # self._sonobuoy_retrieve_result(self) |
| 129 | + except BaseException: |
| 130 | + logger.exception("something went wrong") |
| 131 | + return 112 |
| 132 | + finally: |
| 133 | + self._sonobuoy_delete() |
0 commit comments