From daaae195e46c21d3e2553546a864219e78ef2107 Mon Sep 17 00:00:00 2001
From: Yashvardhan Nanavati <yashn@bu.edu>
Date: Mar 30 2019 08:15:17 +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 b625494..eccbb51 100644
--- a/functional-tests/test_api_v1.py
+++ b/functional-tests/test_api_v1.py
@@ -94,7 +94,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',
@@ -104,7 +105,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 user policies is required.' == r.json()['message']
 
 
 @pytest.mark.smoke
@@ -1290,3 +1291,79 @@ def test_make_a_decision_with_verbose_flag_all_results_returned(
     assert res_data['results'] == list(reversed(results))
     assert len(res_data['waivers']) == len(expected_waivers)
     assert res_data['waivers'] == expected_waivers
+
+
+@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',
+        'user_policies': dedent("""
+            --- !Policy
+            id: "koji-test-policy-missing-results"
+            product_versions:
+             - fedora-30
+            decision_context: koji_build_push_missing_results
+            subject_type: koji_build
+            rules:
+             - !PassingTestCaseRule {test_case_name: dist.rpmdeplint}
+            """)
+
+    }
+    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 user policies') == 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 = {
+        'product_version': 'fedora-31',
+        'subject_type': 'koji_build',
+        'subject_identifier': nvr,
+        'verbose': True,
+        'user_policies': dedent("""
+            --- !Policy
+            id: "user-policy-test"
+            product_versions:
+             - fedora-31
+            subject_type: koji_build
+            rules:
+             - !PassingTestCaseRule {test_case_name: dist.abicheck}
+             - !PassingTestCaseRule {test_case_name: dist.rpmdeplint}
+             - !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 7c472e5..eb6dd14 100644
--- a/greenwave/api_v1.py
+++ b/greenwave/api_v1.py
@@ -7,6 +7,7 @@ from prometheus_client import generate_latest
 from greenwave import __version__
 from greenwave.policies import (summarize_answers,
                                 RemotePolicy,
+                                OnDemandPolicy,
                                 _missing_decision_contexts_in_parent_policies)
 from greenwave.resources import ResultsRetriever, retrieve_waivers
 from greenwave.safe_yaml import SafeYAMLError
@@ -301,9 +302,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 \
+            ('user_policies' not in request.get_json() or
+             not request.get_json()['user_policies']):
+            log.error('Either one of decision context or user policies is required.')
+            raise BadRequest('Either one of decision context or user policies is required.')
     else:
         log.error('No JSON payload in request')
         raise UnsupportedMediaType('No JSON payload in request')
@@ -311,7 +314,21 @@ 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)
+    on_demand_policies = data.get('user_policies', None)
+    if decision_context and on_demand_policies:
+        log.error('Invalid request. Cannot have both decision context and user policies')
+        raise BadRequest('Invalid request. Cannot have both decision context and user policies')
+
+    # Validate YAML
+    if on_demand_policies:
+        try:
+            on_demand_policies = OnDemandPolicy.safe_load_all(on_demand_policies)
+        except SafeYAMLError as e:
+            log.error(str(e))
+            raise BadRequest(str(e))
+
     verbose = data.get('verbose', False)
     if not isinstance(verbose, bool):
         log.error('Invalid verbose flag, must be a bool')
@@ -330,9 +347,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,
diff --git a/greenwave/policies.py b/greenwave/policies.py
index b3b6f4a..810e4ac 100644
--- a/greenwave/policies.py
+++ b/greenwave/policies.py
@@ -592,6 +592,24 @@ class Policy(SafeYAMLObject):
         return 'Policy {!r}'.format(self.id or 'untitled')
 
 
+class OnDemandPolicy(Policy):
+    root_yaml_tag = '!Policy'
+
+    safe_yaml_attributes = {
+        'id': SafeYAMLString(),
+        'product_versions': SafeYAMLList(str),
+        'user_policies': SafeYAMLString(str),
+        'subject_type': SafeYAMLString(),
+        'rules': SafeYAMLList(Rule),
+        'blacklist': SafeYAMLList(str, optional=True),
+        'excluded_packages': SafeYAMLList(str, optional=True),
+        'packages': SafeYAMLList(str, optional=True),
+        'relevance_key': SafeYAMLString(optional=True),
+        'relevance_value': SafeYAMLString(optional=True),
+
+    }
+
+
 class RemotePolicy(Policy):
     root_yaml_tag = '!Policy'
 

