From 8cc28348c081b30c9551dc5bf0d0a05484b67cee Mon Sep 17 00:00:00 2001 From: Luigi Pellecchia Date: Mon, 18 Nov 2024 16:57:02 +0100 Subject: [PATCH] Added KernelCI as external test run plugin to read and import test data. Signed-off-by: Luigi Pellecchia --- Dockerfile-api | 6 +- api/api.py | 195 ++++- api/testrun.py | 13 +- app/src/app/Constants/constants.tsx | 13 +- .../Mapping/Modal/TestResultDetailsModal.tsx | 22 +- .../app/Mapping/Modal/TestResultsModal.tsx | 827 +++++++++++------- 6 files changed, 728 insertions(+), 348 deletions(-) diff --git a/Dockerfile-api b/Dockerfile-api index d36ee2f..854d1ed 100644 --- a/Dockerfile-api +++ b/Dockerfile-api @@ -34,6 +34,8 @@ ENV BASIL_ADMIN_PASSWORD= EXPOSE ${BASIL_API_PORT} CMD echo "BASIL_API_PORT: ${BASIL_API_PORT}" && cd api && \ - gunicorn --access-logfile /var/tmp/gunicorn-access.log \ + gunicorn \ + --timeout 120 \ + --access-logfile /var/tmp/gunicorn-access.log \ --error-logfile /var/tmp/gunicorn-error.log \ - --bind 0.0.0.0:${BASIL_API_PORT} api:app 2>&1 | tee /var/tmp/basil-error.log + --bind 0.0.0.0:${BASIL_API_PORT} api:app 2>&1 | tee /var/tmp/basil-api-error.log diff --git a/api/api.py b/api/api.py index 45ca284..762b4ea 100644 --- a/api/api.py +++ b/api/api.py @@ -934,19 +934,19 @@ def add_test_run_config(dbi, request_data, user): if not check_fields_in_request(tmt_mandatory_fields, request_data): return f"{BAD_REQUEST_MESSAGE} Plugin not supported.", BAD_REQUEST_STATUS - if request_data["plugin"] == "tmt": + if request_data["plugin"] == TestRunner.TMT: if not check_fields_in_request(tmt_mandatory_fields, request_data): return f"{BAD_REQUEST_MESSAGE} tmt miss mandatory fields.", BAD_REQUEST_STATUS - if request_data["plugin"] == "gitlab_ci": + if request_data["plugin"] == TestRunner.GITLAB_CI: if not check_fields_in_request(gitlab_ci_mandatory_fields, request_data): return f"{BAD_REQUEST_MESSAGE} GitlabCI miss mandatory fields.", BAD_REQUEST_STATUS - if request_data["plugin"] == "github_actions": + if request_data["plugin"] == TestRunner.GITHUB_ACTIONS: if not check_fields_in_request(github_actions_mandatory_fields, request_data): return f"{BAD_REQUEST_MESSAGE} Github Actions miss mandatory fields.", BAD_REQUEST_STATUS - if request_data["plugin"] == "kernel_ci": + if request_data["plugin"] == TestRunner.KERNEL_CI: if not check_fields_in_request(kernel_ci_mandatory_fields, request_data): return f"{BAD_REQUEST_MESSAGE} KernelCI miss mandatory fields.", BAD_REQUEST_STATUS @@ -967,7 +967,7 @@ def add_test_run_config(dbi, request_data, user): if config_title == '': return f"{BAD_REQUEST_MESSAGE} Empty Configuration Title.", BAD_REQUEST_STATUS - if plugin == 'tmt': + if plugin == TestRunner.TMT: context_vars = str(request_data['context_vars']).strip() provision_type = str(request_data['provision_type']).strip() provision_guest = str(request_data['provision_guest']).strip() @@ -989,13 +989,13 @@ def add_test_run_config(dbi, request_data, user): except NoResultFound: return f"{BAD_REQUEST_MESSAGE} Unable to find the SSH Key.", BAD_REQUEST_STATUS - elif plugin == 'gitlab_ci': + elif plugin == TestRunner.GITLAB_CI: plugin_vars += ";".join([f"{field}={str(request_data[field]).strip()}" for field in gitlab_ci_mandatory_fields]) - elif plugin == 'github_actions': + elif plugin == TestRunner.GITHUB_ACTIONS: plugin_vars += ";".join([f"{field}={str(request_data[field]).strip()}" for field in github_actions_mandatory_fields]) - elif plugin == 'kernel_ci': + elif plugin == TestRunner.KERNEL_CI: plugin_vars += ";".join([f"{field}={str(request_data[field]).strip()}" for field in kernel_ci_mandatory_fields]) @@ -6294,9 +6294,13 @@ def get(self): class ExternalTestRuns(Resource): def get(self): - mandatory_fields = ["api-id", "plugin", "preset", "ref"] + PARAM_DATE_FORMAT = "%Y-%m-%d" + GITLAB_CI_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" + KERNEL_CI_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S" + mandatory_fields = ["api-id", "plugin", "preset"] args = get_query_string_args(request.args) + if not check_fields_in_request(mandatory_fields, args): return BAD_REQUEST_MESSAGE, BAD_REQUEST_STATUS @@ -6308,7 +6312,6 @@ def get(self): preset = args["preset"].strip() params_strings = [] params = {} - ref = args["ref"].strip() preset_config = None dbi = db_orm.DbInterface(get_db()) @@ -6344,6 +6347,9 @@ def get(self): # Values from test_run_config will override preset values preset_config = tmp[0] + if not preset_config: + return "Preset not found", NOT_FOUND_STATUS + if "params" in args.keys(): params_strings = args["params"].split(";") params_strings = [x for x in params_strings if "=" in x] @@ -6352,9 +6358,10 @@ def get(self): v = param_string.split("=")[1].strip() if k and v: params[k] = v + # Override preset config with params + preset_config[k] = v - if preset_config: - if plugin == "gitlab_ci": + if plugin == TestRunner.GITLAB_CI: gitlab_ci_mandatory_fields = ["private_token", "project_id", "url"] # Skip pending pipelines from the list @@ -6369,15 +6376,19 @@ def get(self): project = gl.projects.get(id=preset_config["project_id"]) job = None + ref = None stage = None + if "job" in preset_config.keys(): if preset_config["job"]: job = preset_config["job"] - if not ref: - if "git_repo_ref" in preset_config.keys(): - if preset_config["git_repo_ref"]: - ref = preset_config["git_repo_ref"] + if "ref" in preset_config.keys(): + if preset_config["ref"]: + ref = preset_config["ref"] + elif "git_repo_ref" in preset_config.keys(): + if preset_config["git_repo_ref"]: + ref = preset_config["git_repo_ref"] if "stage" in preset_config.keys(): if preset_config["stage"]: @@ -6390,20 +6401,53 @@ def get(self): # Filter all_pipelines = [x for x in all_pipelines if x.status in gitlab_ci_valid_status] - param_pipelines = [] + if not all_pipelines: + return [] + pipeline_attrs = all_pipelines[0].__dict__["_attrs"].keys() + params_pipelines = all_pipelines + filtered_by_params = False + filtered_pipelines = [] if params.keys(): - for i in range(len(all_pipelines)): - for k, v in params.items(): - for pipe_kv in all_pipelines[i].variables.list(): - if pipe_kv.key == k: - if v in pipe_kv.value: - param_pipelines.append(all_pipelines[i]) - else: - param_pipelines = all_pipelines + for k, v in params.items(): + if k in pipeline_attrs: + params_pipelines = [x for x in params_pipelines + if x.__dict__["_attrs"][k] == v] + filtered_by_params = True + + if "updated_after" in params.keys(): + if params["updated_after"]: + try: + compare_date = datetime.datetime.strptime(params["updated_after"], PARAM_DATE_FORMAT) + params_pipelines = [x for x in params_pipelines + if datetime.datetime.strptime( + x.updated_at, + GITLAB_CI_DATE_FORMAT + ) >= compare_date] + filtered_by_params = True + except ValueError as e: + print(f"ExternalTestRuns Exception at gitlab ci {e}") + pass + + if "updated_before" in params.keys(): + if params["updated_before"]: + try: + compare_date = datetime.datetime.strptime(params["updated_before"], PARAM_DATE_FORMAT) + params_pipelines = [x for x in params_pipelines + if datetime.datetime.strptime( + x.updated_at, + GITLAB_CI_DATE_FORMAT + ) <= compare_date] + filtered_by_params = True + except ValueError as e: + print(f"ExternalTestRuns Exception at gitlab ci {e}") + pass + + if not filtered_by_params: + params_pipelines = all_pipelines if stage: - for pipeline in param_pipelines: + for pipeline in params_pipelines: pipeline_jobs = pipeline.jobs.list() for pipeline_job in pipeline_jobs: if pipeline_job.__dict__["_attrs"]["stage"] == stage: @@ -6414,19 +6458,30 @@ def get(self): else: filtered_pipelines.append(pipeline) break + else: + for pipeline in params_pipelines: + pipeline_jobs = pipeline.jobs.list() + for pipeline_job in pipeline_jobs: + if job: + if pipeline_job.__dict__["_attrs"]["name"] == job: + filtered_pipelines.append(pipeline) + break + else: + filtered_pipelines.append(pipeline) + break - ret_pipelines = filtered_pipelines - - for p in ret_pipelines: + for p in filtered_pipelines: ret.append({"created_at": p.created_at, "id": p.id, "project": project.name, "ref": p.ref, + "details": p.name, "status": "pass" if p.status == "success" else "fail", "web_url": p.web_url}) - if plugin == "github_actions": + if plugin == TestRunner.GITHUB_ACTIONS: github_actions_mandatory_fields = ["private_token", "url"] + ref = None if not check_fields_in_request(github_actions_mandatory_fields, preset_config, @@ -6446,9 +6501,12 @@ def get(self): repo = url_split[-1] workflows_url = f"https://api.github.com/repos/{owner}/{repo}/actions/runs?" - if not ref: - if "ref" in preset_config.keys(): - ref = preset_config['ref'] + if "ref" in preset_config.keys(): + if preset_config["ref"]: + ref = preset_config["ref"] + elif "git_repo_ref" in preset_config.keys(): + if preset_config["git_repo_ref"]: + ref = preset_config["git_repo_ref"] if ref: workflows_url += f"&branch={ref}" @@ -6483,9 +6541,80 @@ def get(self): "id": p['id'], "project": f"{owner}/{repo}", "ref": p['head_branch'], + "details": p['name'], "status": "pass" if p['conclusion'] == "success" else "fail", "web_url": f"{preset_config['url']}/actions/runs/{p['id']}"}) + if plugin == TestRunner.KERNEL_CI: + NODES_ENDPOINT = "nodes" + dashboard_base_url = "https://dashboard.kernelci.org/api/tests/test/maestro:" + kernel_ci_mandatory_fields = ["private_token", "url"] + + if not check_fields_in_request(kernel_ci_mandatory_fields, + preset_config, + allow_empty_string=False): + return BAD_REQUEST_MESSAGE, BAD_REQUEST_STATUS + + if preset_config['url'].endswith("/"): + preset_config['url'] = preset_config['url'][:-1] + + # We use double underscore in the param key for kernelCI + # that should be replace with a . when used in the kernelCI API + for i in range(len(params_strings)): + kv = params_strings[i].split("=") + k = kv[0] + v = kv[1] + if k == "created_after": + try: + compare_date = datetime.datetime.strptime(params["created_after"], PARAM_DATE_FORMAT) + compare_date_str = compare_date.strftime(KERNEL_CI_DATE_FORMAT) + params_strings[i] = f"created__gt={compare_date_str}" + except ValueError as e: + print(f"ExternalTestRuns Exception at KernelCI {e}") + pass + elif k == "created_before": + try: + compare_date = datetime.datetime.strptime(params["created_before"], PARAM_DATE_FORMAT) + compare_date_str = compare_date.strftime(KERNEL_CI_DATE_FORMAT) + params_strings[i] = f"created__lt={compare_date_str}" + except ValueError as e: + print(f"ExternalTestRuns Exception at KernelCI {e}") + pass + else: + params_strings[i] = f"{k.replace('__', '.')}={v}" + + kernel_ci_url = f"{preset_config['url']}/{NODES_ENDPOINT}?kind=test&state=done" + if params_strings: + kernel_ci_url += f"&{'&'.join(params_strings)}" + + headers = { + "Authorization": f"Bearer {preset_config['private_token']}", + } + + try: + kernel_ci_request = urllib.request.Request( + url=kernel_ci_url, + headers=headers + ) + + response_data = urllib.request.urlopen(kernel_ci_request).read() + content = json.loads(response_data.decode("utf-8")) + except Exception as e: + return f"{BAD_REQUEST_MESSAGE} Unable to read workflows {e}", BAD_REQUEST_STATUS + else: + ret_pipelines = content["items"] + + for p in ret_pipelines: + project = p["data"]["kernel_revision"]["tree"] + branch = p["data"]["kernel_revision"]["branch"] + ret.append({"created_at": p['created'], + "id": p['id'], + "project": project, + "ref": branch, + "details": p['name'], + "status": "pass" if p['result'] == "pass" else "fail", + "web_url": f"{dashboard_base_url}{p['id']}"}) + return ret diff --git a/api/testrun.py b/api/testrun.py index 8308f52..eb247e1 100644 --- a/api/testrun.py +++ b/api/testrun.py @@ -51,10 +51,15 @@ class TestRunner: STATUS_RUNNING = 'running' STATUS_COMPLETED = 'completed' - test_run_plugin_models = {'github_actions': TestRunnerGithubActionsPlugin, - 'gitlab_ci': TestRunnerGitlabCIPlugin, - 'KernelCI': None, - 'tmt': TestRunnerTmtPlugin} + KERNEL_CI = 'KernelCI' + GITLAB_CI = 'gitlab_ci' + GITHUB_ACTIONS = 'github_actions' + TMT = 'tmt' + + test_run_plugin_models = {GITHUB_ACTIONS: TestRunnerGithubActionsPlugin, + GITLAB_CI: TestRunnerGitlabCIPlugin, + KERNEL_CI: None, + TMT: TestRunnerTmtPlugin} runner_plugin = None config = {} diff --git a/app/src/app/Constants/constants.tsx b/app/src/app/Constants/constants.tsx index 3ca2ad4..e4394b0 100644 --- a/app/src/app/Constants/constants.tsx +++ b/app/src/app/Constants/constants.tsx @@ -37,11 +37,16 @@ export const provision_type = [ { value: 'connect', label: 'SSH', disabled: false } ] +export const gitlab_ci_plugin = 'gitlab_ci' +export const github_actions_plugin = 'github_actions' +export const kernel_ci_plugin = 'KernelCI' +export const tmt_plugin = 'tmt' + export const test_run_plugins = [ - { value: 'tmt', label: 'tmt', disabled: false }, - { value: 'github_actions', label: 'github actions', disabled: false }, - { value: 'gitlab_ci', label: 'gitlab ci', disabled: false }, - { value: 'kernel_ci', label: 'KernelCI', disabled: false } + { value: tmt_plugin, label: 'tmt', disabled: false, trigger: true }, + { value: github_actions_plugin, label: 'github actions', disabled: false, trigger: true }, + { value: gitlab_ci_plugin, label: 'gitlab ci', disabled: false, trigger: true }, + { value: kernel_ci_plugin, label: 'KernelCI', disabled: false, trigger: false } ] export const spdx_relations = [ diff --git a/app/src/app/Mapping/Modal/TestResultDetailsModal.tsx b/app/src/app/Mapping/Modal/TestResultDetailsModal.tsx index 7ea52b3..aa67d77 100644 --- a/app/src/app/Mapping/Modal/TestResultDetailsModal.tsx +++ b/app/src/app/Mapping/Modal/TestResultDetailsModal.tsx @@ -179,14 +179,20 @@ export const TestResultDetailsModal: React.FunctionComponent - -   + {currentTestResult.status != 'completed' ? ( + <> + +   + + ) : ( + '' + )} - - -
- - - - - - - - - - - - - - - {testResults && - testResults.map((testResult) => ( - handleSelectedTestResult(testResult)} - isRowSelected={selectedTestResult === testResult} - > - - - - + + + + + ))} + +
{columnNames.id}{columnNames.title}{columnNames.sut}{columnNames.result}{columnNames.date}{columnNames.bugs_fixes}{columnNames.actions}
{testResult.id}{testResult.title} - {(() => { - if (testResult.config.plugin == 'tmt') { - if (testResult.config.provision_type == 'connect') { - return testResult.config.provision_guest - } else { - return 'container' - } - } else { - return testResult.config.plugin - } - })()} - - {(() => { - if (testResult?.result == null) { - return ( - - ) + - - - + } + })()} + {testResult.created_at} + {testResult.bugs?.length > 0 ? : ''} +   + {testResult.fixes?.length > 0 ? : ''} + + {(() => { + if (api?.permissions.indexOf('w') >= 0) { + for (let iPlugin = 0; iPlugin < Constants.test_run_plugins.length; iPlugin++) { + if (Constants.test_run_plugins[iPlugin].value == testResult.config.plugin) { + if (Constants.test_run_plugins[iPlugin].trigger == true) { + return ( + + ) + } + } + } + return '' + } else { + return '' + } + })()} + + {(() => { + if (api?.permissions.indexOf('w') >= 0) { + return ( + <> + + + + ) + } else { + return '' + } + })()} +
+
+ + + )