|
| 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 |
0 commit comments