From 98c2c341ddc24488b4956c11e3aeaff222c284c9 Mon Sep 17 00:00:00 2001 From: Yashvardhan Nanavati Date: Apr 12 2019 03:53:43 +0000 Subject: Support for on-demand policy in Greenwave --- diff --git a/functional-tests/test_api_v1.py b/functional-tests/test_api_v1.py index a90e0ce..1ecb8e6 100644 --- a/functional-tests/test_api_v1.py +++ b/functional-tests/test_api_v1.py @@ -96,7 +96,8 @@ def test_cannot_make_decision_without_product_version(requests_session, greenwav @pytest.mark.smoke -def test_cannot_make_decision_without_decision_context(requests_session, greenwave_server): +def test_cannot_make_decision_without_decision_context_and_user_policies( + requests_session, greenwave_server): data = { 'product_version': 'fedora-26', 'subject_type': 'bodhi_update', @@ -106,7 +107,7 @@ def test_cannot_make_decision_without_decision_context(requests_session, greenwa headers={'Content-Type': 'application/json'}, data=json.dumps(data)) assert r.status_code == 400 - assert 'Missing required decision context' == r.json()['message'] + assert 'Either one of decision_context or rules is required.' == r.json()['message'] @pytest.mark.smoke @@ -1168,7 +1169,7 @@ def test_decision_on_redhat_module(requests_session, greenwave_server, testdatab 'outcome': 'PASSED', 'data': {'item': nvr, 'type': 'redhat-module'} } - result = testdatabuilder._create_result(new_result_data) # noqa + result = testdatabuilder._create_result(new_result_data) # noqa data = { 'decision_context': 'osci_compose_gate_modules', 'product_version': 'rhel-8', @@ -1300,7 +1301,7 @@ def test_verbose_retrieve_latest_results_scenario(requests_session, greenwave_se results = [] for scenario in ['fedora.universal.x86_64.uefi', 'fedora.universal.x86_64.64bit']: results.append(testdatabuilder.create_compose_result(compose_id=nvr, - testcase_name='testcase_name', outcome='PASSED', scenario=scenario)) + testcase_name='testcase_name', outcome='PASSED', scenario=scenario)) data = { 'decision_context': 'compose_test_scenario', 'product_version': 'fedora-29', @@ -1347,3 +1348,78 @@ def test_api_returns_not_repeated_waiver_in_verbose_info( assert r_.status_code == 200 res_data = r_.json() assert len(res_data['waivers']) == 1 + + +@pytest.mark.smoke +def test_cannot_make_decision_with_both_decision_context_and_user_policies( + requests_session, greenwave_server): + data = { + 'product_version': 'fedora-26', + 'subject_type': 'bodhi_update', + 'subject_identifier': 'FEDORA-2018-ec7cb4d5eb', + 'decision_context': 'koji_build_push_missing_results', + 'rules': [ + { + 'type': 'PassingTestCaseRule', + 'test_case_name': 'osci.brew-build.rpmdeplint.functional' + }, + ], + } + r = requests_session.post(greenwave_server + 'api/v1.0/decision', + headers={'Content-Type': 'application/json'}, + data=json.dumps(data)) + assert r.status_code == 400 + assert ('Invalid request. Cannot have both' + ' decision_context and rules') == r.json()['message'] + + +def test_make_a_decision_with_verbose_flag_on_demand_policy( + requests_session, greenwave_server, testdatabuilder): + nvr = testdatabuilder.unique_nvr() + results = [] + expected_waivers = [] + # First one failed but was waived + results.append(testdatabuilder.create_result(item=nvr, + testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], + outcome='FAILED')) + expected_waivers.append( + testdatabuilder.create_waiver(nvr=nvr, + product_version='fedora-31', + testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], + comment='This is fine')) + for testcase_name in TASKTRON_RELEASE_CRITICAL_TASKS[1:]: + results.append(testdatabuilder.create_result(item=nvr, + testcase_name=testcase_name, + outcome='PASSED')) + + data = { + 'id': 'on_demand', + 'product_version': 'fedora-31', + 'subject_type': 'koji_build', + 'subject_identifier': nvr, + 'verbose': True, + 'rules': [ + { + 'type': 'PassingTestCaseRule', + 'test_case_name': 'dist.abicheck' + }, + { + 'type': 'PassingTestCaseRule', + 'test_case_name': 'dist.rpmdeplint' + }, + { + 'type': 'PassingTestCaseRule', + 'test_case_name': 'dist.upgradepath' + }, + ], + } + r = requests_session.post(greenwave_server + 'api/v1.0/decision', + headers={'Content-Type': 'application/json'}, + data=json.dumps(data)) + assert r.status_code == 200 + res_data = r.json() + + assert len(res_data['results']) == len(results) + assert res_data['results'] == list(reversed(results)) + assert len(res_data['waivers']) == len(expected_waivers) + assert res_data['waivers'] == expected_waivers diff --git a/greenwave/api_v1.py b/greenwave/api_v1.py index aac01f4..c112f0b 100644 --- a/greenwave/api_v1.py +++ b/greenwave/api_v1.py @@ -7,7 +7,9 @@ from prometheus_client import generate_latest from greenwave import __version__ from greenwave.policies import (summarize_answers, RemotePolicy, - _missing_decision_contexts_in_parent_policies) + OnDemandPolicy, + _missing_decision_contexts_in_parent_policies, + Rule) from greenwave.resources import ResultsRetriever, retrieve_waivers from greenwave.safe_yaml import SafeYAMLError from greenwave.utils import insert_headers, jsonp @@ -301,9 +303,11 @@ def make_decision(): log.error('Missing required product version') raise BadRequest('Missing required product version') if ('decision_context' not in request.get_json() or - not request.get_json()['decision_context']): - log.error('Missing required decision context') - raise BadRequest('Missing required decision context') + not request.get_json()['decision_context']) and \ + ('rules' not in request.get_json() or + not request.get_json()['rules']): + log.error('Either one of decision_context or rules is required.') + raise BadRequest('Either one of decision_context or rules is required.') else: log.error('No JSON payload in request') raise UnsupportedMediaType('No JSON payload in request') @@ -311,7 +315,19 @@ def make_decision(): data = request.get_json() log.debug('New decision request for data: %s', data) product_version = data['product_version'] - decision_context = data['decision_context'] + + decision_context = data.get('decision_context', None) + rules = data.get('rules', []) + if decision_context and rules: + log.error('Invalid request. Cannot have both decision_context and rules') + raise BadRequest('Invalid request. Cannot have both decision_context and rules') + + on_demand_policies = [] + if rules: + on_demand_policy = OnDemandPolicy() + on_demand_policy._create_on_demand_policy(data) + on_demand_policies.append(on_demand_policy) + verbose = data.get('verbose', False) if not isinstance(verbose, bool): log.error('Invalid verbose flag, must be a bool') @@ -330,9 +346,10 @@ def make_decision(): verify=current_app.config['REQUESTS_VERIFY'], url=current_app.config['RESULTSDB_API_URL']) + policies = on_demand_policies or current_app.config['policies'] for subject_type, subject_identifier in _decision_subjects_for_request(data): subject_policies = [ - policy for policy in current_app.config['policies'] + policy for policy in policies if policy.matches( decision_context=decision_context, product_version=product_version, @@ -376,6 +393,14 @@ def make_decision(): [answer.to_json() for answer in answers if not answer.is_satisfied], } + # Check if on-demand policy was specified + if rules: + response.update({ + 'product_version': product_version, + 'subject_identifier': data['subject_identifier'], + 'subject_type': data['subject_type'] + }) + if verbose: # removing duplicated elements... response.update({ diff --git a/greenwave/policies.py b/greenwave/policies.py index b3b6f4a..b39766c 100644 --- a/greenwave/policies.py +++ b/greenwave/policies.py @@ -306,6 +306,26 @@ class Rule(SafeYAMLObject): """ return True + @staticmethod + def process_on_demand_rules(rules): + """ + Validates rule type and creates objects for them. + + """ + processed_rules = [] + for rule in rules: + if rule['type'] not in ('RemoteRule', 'PassingTestCaseRule'): + raise ValueError('Invalid rule type {}'.format(rule['type'])) + + if rule['type'] == 'RemoteRule': + processed_rules.append(RemoteRule()) + else: + processed_rules.append(PassingTestCaseRule()) + processed_rules[-1].test_case_name = rule['test_case_name'] + processed_rules[-1].scenario = rule.get('scenario') + + return processed_rules + def waives_invalid_gating_yaml(waiver, subject_type, subject_identifier): return (waiver['testcase'] == 'invalid-gating-yaml' and @@ -592,6 +612,44 @@ class Policy(SafeYAMLObject): return 'Policy {!r}'.format(self.id or 'untitled') +class OnDemandPolicy(Policy): + root_yaml_tag = '!Policy' + safe_yaml_attributes = {} + + def __init__(self): + self.id = None + self.product_versions = None + self.subject_type = None + self.rules = None + self.blacklist = None + self.excluded_packages = None + self.packages = None + self.relevance_key = None + + def _create_on_demand_policy(self, data_dict): + # Validate the data before processing. + # self.__validate_attributes(data_dict) + + self.id = data_dict.get('id') + self.product_versions = [data_dict['product_version']] + self.subject_type = data_dict['subject_type'] + self.rules = Rule.process_on_demand_rules(data_dict['rules']) + self.blacklist = data_dict.get('blacklist', []) + self.excluded_packages = data_dict.get('excluded_packages', []) + self.packages = data_dict.get('packages', []) + self.relevance_key = data_dict.get('relevance_key') + + def __validate_attributes(self, data_dict): + """ Validates types of the attributes. """ + list_attributes = ['product_versions', 'rules', 'excluded_packages', 'packages'] + + for attribute in data_dict.keys(): + if attribute in list_attributes and not isinstance(getattr(self, attribute), list): + raise TypeError('{} should be a list.'.format(attribute)) + elif not isinstance(getattr(self, attribute), str): + raise TypeError('{} should be a string.'.format(attribute)) + + class RemotePolicy(Policy): root_yaml_tag = '!Policy'