#184 make Greenwave aware of specific subject types and identifiers
Merged 3 years ago by dcallagh. Opened 3 years ago by dcallagh.
dcallagh/greenwave subject-type  into  master

@@ -9,6 +9,7 @@ 

  product_versions:

    - fedora-26

  decision_context: bodhi_update_push_stable

+ subject_type: koji_build

  blacklist:

    # see the excluded list for dist.abicheck 

    # https://infrastructure.fedoraproject.org/cgit/ansible.git/tree/roles/taskotron/taskotron-trigger/templates/trigger_rules.yml.j2#n17
@@ -27,6 +28,7 @@ 

  product_versions:

    - fedora-*

  decision_context: bodhi_update_push_testing

+ subject_type: koji_build

  blacklist: []

  rules:

    - !PassingTestCaseRule {test_case_name: dist.rpmdeplint}
@@ -35,6 +37,7 @@ 

  product_versions:

    - fedora-26

  decision_context: bodhi_update_push_stable

+ subject_type: koji_build

  blacklist: []

  rules:

    - !PassingTestCaseRule {test_case_name: dist.rpmdeplint}
@@ -44,6 +47,7 @@ 

  product_versions:

    - fedora-rawhide

  decision_context: rawhide_compose_sync_to_mirrors

+ subject_type: compose

  blacklist: []

  rules:

    - !PassingTestCaseRule {test_case_name: compose.install_no_user, scenario: scenario1}
@@ -53,6 +57,7 @@ 

  product_versions:

    - fedora-26

  decision_context: bodhi_update_push_stable_with_remoterule

+ subject_type: bodhi_update

  blacklist: []

  rules:

    - !RemoteOriginalSpecNvrRule {}
@@ -64,5 +69,6 @@ 

  product_versions:

  - fedora-24

  decision_context: bodhi_update_push_stable

+ subject_type: bodhi_update

  blacklist: []

  rules: []

@@ -0,0 +1,21 @@ 

+ # This is just a hypothetical policy which could be used for Red Hat products.

+ --- !Policy

+ id: "osci_compose"

+ product_versions:

+ - rhel-something

+ decision_context: osci_compose_gate

+ subject_type: koji_build

+ blacklist: []

+ rules:

+ - !PackageSpecificBuild {

+     test_case_name: osci.brew-build.tier0.functional,

+     repos: [

+       "avahi",

+       "cockpit",

+       "checkpolicy",

+       "libsemanage",

+       "libselinux",

+       "libsepol",

+       "policycoreutils",

+     ]

+   }

file modified
+52 -14
@@ -22,6 +22,7 @@ 

     --- !Policy

     id: taskotron_release_critical_tasks

     decision_context: bodhi_update_push_stable

+    subject_type: bodhi_update

     product_versions:

     - fedora-26

     - fedora-27
@@ -59,6 +60,15 @@ 

     passes this value when it asks Greenwave to decide whether a Bodhi update

     is ready to be pushed to the stable repositories.

  

+ ``subject_type``

+    When you ask Greenwave for a decision, you ask it about a specific software

+    artefact (the "subject" of the decision). Each policy applies to some type

+    of software artefact -- in this example, the policy applies to Bodhi

+    updates.

+ 

+    The subject type must be one of the fixed set of types known to Greenwave.

+    See the :ref:`subject-types` section below for a list of possible types.

+ 

  ``product_versions``

     A policy applies to one or more "product versions". When you ask Greenwave

     for a decision, you must tell it which product version you are working
@@ -83,7 +93,7 @@ 

     map, tagged with the rule type.

  

     Currently there are a few rule types, ``PassingTestCaseRule`` being one of

-    them.  See the section of Rule Types, below for a full list.

+    them.  See the :ref:`rule-types` section below for a full list.

  

  ``blacklist``

     A list of binary RPM package names which are exempted from this policy.
@@ -91,11 +101,44 @@ 

     The blacklist only takes effect when Greenwave is making a decision about

     subjects with ``"item": "koji_build"``.

  

+ .. _Koji: https://pagure.io/koji

  .. _Bodhi: https://github.com/fedora-infra/bodhi

  .. _Product Definition Center: https://github.com/product-definition-center/product-definition-center

  

  

- Rule Types

+ .. _subject-types:

+ 

+ Subject types

+ =============

+ 

+ Greenwave can make decisions about the following types of software artefacts:

+ 

+ ``koji_build``

+    A build stored in the `Koji`_ build system. Builds are identified by their

+    Name-Version-Release (NVR) identifier, as in ``glibc-2.26-27.fc27``.

+    Note that Koji identifies builds by the NVR of their source RPM,

+    regardless which binary packages were produced in the build.

+ 

+ ``bodhi_update``

+    A distribution update in `Bodhi`_. Updates are identified by their Bodhi

+    update id, as in ``FEDORA-2018-ec7cb4d5eb``.

+ 

+    A Bodhi update contains one or more Koji builds. When Greenwave makes a

+    decision about a Bodhi update, it *also* considers any policies which apply

+    to Koji builds in that update.

+ 

+ ``compose``

+    A distribution compose. The compose tool (typically Pungi) takes a snapshot

+    of the distribution at a point in time, and produces a directory hierarchy

+    containing packages, installer images, and other metadata. Composes are

+    identified by the compose id in their metadata, which is typically also

+    reflected in their directory name, for example

+    ``Fedora-Rawhide-20170508.n.0``.

+ 

+ 

+ .. _rule-types:

+ 

+ Rule types

  ==========

  

  PassingTestCaseRule
@@ -111,20 +154,15 @@ 

  

     Just like the ``PassingTestCaseRule``, the ``PackageSpecificBuild`` rule

     requires that a given ``test_case_name`` is passing, but only for certain

-    packages (listed in the ``repos`` argument).  The configured package names

-    in the ``repos`` list may contain wildcards to, for instance, write a rule

-    requiring a certain test must pass for all `python-*` packages.

- 

-    At query time, the package name is parsed from an assumed `nvr` in the

-    ``item`` of the subject.

- 

+    source package names (listed in the ``repos`` argument).  The configured

+    package names in the ``repos`` list may contain wildcards to, for instance,

+    write a rule requiring a certain test must pass for all `python-*`

+    packages.

  

- FedoraAtomicCi

- --------------

+    This rule type can only be used if the policy's subject type is

+    ``koji_build``.

  

-    This rule is nearly identical to the ``PackageSpecificBuild`` rule, except

-    that at query time, the package name is parsed from an assumed `nvr` in the

-    ``original_spec_nvr`` of the subject.

+    ``FedoraAtomicCi`` is a backwards compatibility alias for this rule type.

  

  .. _remote-original-spec-nvr-rule:

  

file modified
+62 -24
@@ -134,6 +134,25 @@ 

  

  

  @pytest.yield_fixture(scope='session')

+ def bodhi():

+     if 'BODHI_TEST_URL' in os.environ:

+         yield os.environ['BODHI_TEST_URL']

+     else:

+         # Start fake Bodhi as a subprocess

+         p = subprocess.Popen(['gunicorn',

+                               '--bind=127.0.0.1:5677',

+                               '--access-logfile=-',

+                               '--pythonpath=' + os.path.dirname(__file__),

+                               'fake_bodhi:application'])

+         log.debug('Started fake Bodhi as pid %s', p.pid)

+         wait_for_listen(5677)

+         yield 'http://localhost:5677/'

+         log.debug('Terminating fake Bodhi pid %s', p.pid)

+         p.terminate()

+         p.wait()

+ 

+ 

+ @pytest.yield_fixture(scope='session')

  def distgit_server(tmpdir_factory):

      """ Creating a fake dist-git process. It is just a serving some files in a tmp dir """

      tmp_dir = tmpdir_factory.mktemp('distgit')
@@ -149,7 +168,7 @@ 

  

  

  @pytest.yield_fixture(scope='session')

- def greenwave_server(tmpdir_factory, resultsdb_server, waiverdb_server):

+ def greenwave_server(tmpdir_factory, resultsdb_server, waiverdb_server, bodhi):

      if 'GREENWAVE_TEST_URL' in os.environ:

          yield os.environ['GREENWAVE_TEST_URL']

      else:
@@ -164,7 +183,8 @@ 

              }

              RESULTSDB_API_URL = '%sapi/v2.0'

              WAIVERDB_API_URL = '%sapi/v1.0'

-             """ % (cache_file.strpath, resultsdb_server, waiverdb_server)))

+             BODHI_URL = %r

+             """ % (cache_file.strpath, resultsdb_server, waiverdb_server, bodhi)))

  

          # We also update the config file for *this* process, as well as the server subprocess,

          # because the fedmsg consumer tests actually invoke the handler code in-process.
@@ -200,19 +220,29 @@ 

      ResultsDB and WaiverDB.

      """

  

-     def __init__(self, requests_session, resultsdb_url, waiverdb_url, distgit_url):

+     def __init__(self, requests_session, resultsdb_url, waiverdb_url, bodhi_url, distgit_url):

          self.requests_session = requests_session

          self.resultsdb_url = resultsdb_url

          self.waiverdb_url = waiverdb_url

+         self.bodhi_url = bodhi_url

          self.distgit_url = distgit_url

          self._counter = itertools.count(1)

  

-     def unique_nvr(self):

-         return 'glibc-1.0-{}.el7'.format(self._counter.next())

+     def unique_nvr(self, name='glibc'):

+         return '{}-1.0-{}.el7'.format(name, self._counter.next())

  

      def unique_compose_id(self):

          return 'Fedora-9000-19700101.n.{}'.format(self._counter.next())

  

+     def _create_result(self, data):

+         response = self.requests_session.post(

+             self.resultsdb_url + 'api/v2.0/results',

+             headers={'Content-Type': 'application/json'},

+             timeout=TEST_HTTP_TIMEOUT,

+             data=json.dumps(data))

+         response.raise_for_status()

+         return response.json()

+ 

      def create_compose_result(self, compose_id, testcase_name, outcome, scenario=None):

          data = {

              'testcase': {'name': testcase_name},
@@ -221,13 +251,15 @@ 

          }

          if scenario:

              data['data']['scenario'] = scenario

-         response = self.requests_session.post(

-             self.resultsdb_url + 'api/v2.0/results',

-             headers={'Content-Type': 'application/json'},

-             timeout=TEST_HTTP_TIMEOUT,

-             data=json.dumps(data))

-         response.raise_for_status()

-         return response.json()

+         return self._create_result(data)

+ 

+     def create_koji_build_result(self, nvr, testcase_name, outcome, type_='koji_build'):

+         data = {

+             'testcase': {'name': testcase_name},

+             'outcome': outcome,

+             'data': {'item': nvr, 'type': type_},

+         }

+         return self._create_result(data)

  

      def create_result(self, item, testcase_name, outcome, scenario=None, key=None):

          data = {
@@ -240,18 +272,13 @@ 

              data['data'] = {key: item}

          if scenario:

              data['data']['scenario'] = scenario

-         response = self.requests_session.post(

-             self.resultsdb_url + 'api/v2.0/results',

-             headers={'Content-Type': 'application/json'},

-             timeout=TEST_HTTP_TIMEOUT,

-             data=json.dumps(data))

-         response.raise_for_status()

-         return response.json()

+         return self._create_result(data)

  

-     def create_waiver(self, result, product_version, comment, waived=True):

+     def create_waiver(self, nvr, testcase_name, product_version, comment, waived=True):

          data = {

-             'subject': result['subject'],

-             'testcase': result['testcase'],

+             'subject_type': 'koji_build',

+             'subject_identifier': nvr,

+             'testcase': testcase_name,

              'product_version': product_version,

              'waived': waived,

              'comment': comment
@@ -267,7 +294,18 @@ 

          response.raise_for_status()

          return response.json()

  

+     def create_bodhi_update(self, build_nvrs):

+         data = {'builds': [{'nvr': nvr} for nvr in build_nvrs]}

+         response = self.requests_session.post(

+             self.bodhi_url + 'updates/',

+             headers={'Content-Type': 'application/json'},

+             timeout=TEST_HTTP_TIMEOUT,

+             data=json.dumps(data))

+         response.raise_for_status()

+         return response.json()

+ 

  

  @pytest.fixture(scope='session')

- def testdatabuilder(requests_session, resultsdb_server, waiverdb_server, distgit_server):

-     return TestDataBuilder(requests_session, resultsdb_server, waiverdb_server, distgit_server)

+ def testdatabuilder(requests_session, resultsdb_server, waiverdb_server, bodhi, distgit_server):

+     return TestDataBuilder(requests_session, resultsdb_server, waiverdb_server, bodhi,

+                            distgit_server)

@@ -14,6 +14,8 @@ 

          testdatabuilder):

      load_config.return_value = {'greenwave_api_url': greenwave_server + 'api/v1.0'}

      nvr = testdatabuilder.unique_nvr()

+     update = testdatabuilder.create_bodhi_update(build_nvrs=[nvr])

+     updateid = update['updateid']

      result = testdatabuilder.create_result(item=nvr,

                                             testcase_name='dist.rpmdeplint',

                                             outcome='PASSED')
@@ -42,84 +44,196 @@ 

      assert handler.topic == ['topic_prefix.environment.taskotron.result.new']

      handler.consume(message)

  

-     # get old decision

-     data = {

-         'decision_context': 'bodhi_update_push_stable',

-         'product_version': 'fedora-26',

-         'subject': [{'item': nvr, 'type': 'koji_build'}],

-         'ignore_result': [result['id']]

-     }

-     r = requests_session.post(greenwave_server + 'api/v1.0/decision',

-                               headers={'Content-Type': 'application/json'},

-                               data=json.dumps(data))

-     assert r.status_code == 200

-     old_decision = r.json()

-     # should have two messages published as we have two decision contexts applicable to

-     # this subject.

-     first_msg = {

+     # We expect 4 messages altogether.

+     assert len(mock_fedmsg.mock_calls) == 4

+     assert all(call[2]['topic'] == 'decision.update' for call in mock_fedmsg.mock_calls)

+     actual_msgs_sent = [call[2]['msg'] for call in mock_fedmsg.mock_calls]

+     assert actual_msgs_sent[0] == {

          'policies_satisfied': False,

          'decision_context': 'bodhi_update_push_stable',

          'product_version': 'fedora-26',

          'unsatisfied_requirements': [

              {

                  'testcase': 'dist.abicheck',

-                 'item': {

-                     'item': nvr,

-                     'type': 'koji_build'

-                 },

+                 'item': {'item': nvr, 'type': 'koji_build'},

+                 'subject_type': 'koji_build',

+                 'subject_identifier': nvr,

                  'type': 'test-result-missing',

                  'scenario': None,

              },

              {

                  'testcase': 'dist.upgradepath',

-                 'item': {

-                     'item': nvr,

-                     'type': 'koji_build'

-                 },

+                 'item': {'item': nvr, 'type': 'koji_build'},

+                 'subject_type': 'koji_build',

+                 'subject_identifier': nvr,

                  'type': 'test-result-missing',

                  'scenario': None,

              }

          ],

          'summary': '2 of 3 required test results missing',

          'subject': [

-             {

-                 'item': nvr,

-                 'type': 'koji_build'

-             }

+             {'item': nvr, 'type': 'koji_build'},

          ],

+         'subject_type': 'koji_build',

+         'subject_identifier': nvr,

          'applicable_policies': ['taskotron_release_critical_tasks_with_blacklist',

                                  'taskotron_release_critical_tasks'],

-         'previous': old_decision,

+         'previous': {

+             'applicable_policies': ['taskotron_release_critical_tasks_with_blacklist',

+                                     'taskotron_release_critical_tasks'],

+             'policies_satisfied': False,

+             'summary': u'3 of 3 required test results missing',

+             'unsatisfied_requirements': [

+                 {

+                     'testcase': 'dist.abicheck',

+                     'item': {'item': nvr, 'type': 'koji_build'},

+                     'subject_type': 'koji_build',

+                     'subject_identifier': nvr,

+                     'type': 'test-result-missing',

+                     'scenario': None,

+                 },

+                 {

+                     'testcase': 'dist.rpmdeplint',

+                     'item': {'item': nvr, 'type': 'koji_build'},

+                     'subject_type': 'koji_build',

+                     'subject_identifier': nvr,

+                     'type': 'test-result-missing',

+                     'scenario': None,

+                 },

+                 {

+                     'testcase': 'dist.upgradepath',

+                     'item': {'item': nvr, 'type': 'koji_build'},

+                     'subject_type': 'koji_build',

+                     'subject_identifier': nvr,

+                     'type': 'test-result-missing',

+                     'scenario': None,

+                 },

+             ],

+         },

      }

-     mock_fedmsg.assert_any_call(topic='decision.update', msg=first_msg)

-     # get the old decision for the second policy

-     data = {

+     assert actual_msgs_sent[1] == {

+         'policies_satisfied': True,

          'decision_context': 'bodhi_update_push_testing',

+         'product_version': 'fedora-*',

+         'unsatisfied_requirements': [],

+         'summary': 'all required tests passed',

+         'subject': [

+             {'item': nvr, 'type': 'koji_build'},

+         ],

+         'subject_type': 'koji_build',

+         'subject_identifier': nvr,

+         'applicable_policies': ['taskotron_release_critical_tasks_for_testing'],

+         'previous': {

+             'applicable_policies': ['taskotron_release_critical_tasks_for_testing'],

+             'policies_satisfied': False,

+             'summary': '1 of 1 required test results missing',

+             'unsatisfied_requirements': [

+                 {

+                     'testcase': 'dist.rpmdeplint',

+                     'item': {'item': nvr, 'type': 'koji_build'},

+                     'subject_type': 'koji_build',

+                     'subject_identifier': nvr,

+                     'type': 'test-result-missing',

+                     'scenario': None,

+                 },

+             ],

+         },

+     }

+     assert actual_msgs_sent[2] == {

+         'policies_satisfied': False,

+         'decision_context': 'bodhi_update_push_stable',

          'product_version': 'fedora-26',

-         'subject': [{'item': nvr, 'type': 'koji_build'}],

-         'ignore_result': [result['id']]

+         'unsatisfied_requirements': [

+             {

+                 'testcase': 'dist.abicheck',

+                 'item': {'item': nvr, 'type': 'koji_build'},

+                 'subject_type': 'koji_build',

+                 'subject_identifier': nvr,

+                 'type': 'test-result-missing',

+                 'scenario': None,

+             },

+             {

+                 'testcase': 'dist.upgradepath',

+                 'item': {'item': nvr, 'type': 'koji_build'},

+                 'subject_type': 'koji_build',

+                 'subject_identifier': nvr,

+                 'type': 'test-result-missing',

+                 'scenario': None,

+             }

+         ],

+         'summary': '2 of 3 required test results missing',

+         'subject': [

+             {'item': updateid, 'type': 'bodhi_update'},

+             {'item': nvr, 'type': 'koji_build'},

+             {'original_spec_nvr': nvr},

+         ],

+         'subject_type': 'bodhi_update',

+         'subject_identifier': updateid,

+         'applicable_policies': ['taskotron_release_critical_tasks_with_blacklist',

+                                 'taskotron_release_critical_tasks'],

+         'previous': {

+             'applicable_policies': ['taskotron_release_critical_tasks_with_blacklist',

+                                     'taskotron_release_critical_tasks'],

+             'policies_satisfied': False,

+             'summary': u'3 of 3 required test results missing',

+             'unsatisfied_requirements': [

+                 {

+                     'testcase': 'dist.abicheck',

+                     'item': {'item': nvr, 'type': 'koji_build'},

+                     'subject_type': 'koji_build',

+                     'subject_identifier': nvr,

+                     'type': 'test-result-missing',

+                     'scenario': None,

+                 },

+                 {

+                     'testcase': 'dist.rpmdeplint',

+                     'item': {'item': nvr, 'type': 'koji_build'},

+                     'subject_type': 'koji_build',

+                     'subject_identifier': nvr,

+                     'type': 'test-result-missing',

+                     'scenario': None,

+                 },

+                 {

+                     'testcase': 'dist.upgradepath',

+                     'item': {'item': nvr, 'type': 'koji_build'},

+                     'subject_type': 'koji_build',

+                     'subject_identifier': nvr,

+                     'type': 'test-result-missing',

+                     'scenario': None,

+                 },

+             ],

+         },

      }

-     r = requests_session.post(greenwave_server + 'api/v1.0/decision',

-                               headers={'Content-Type': 'application/json'},

-                               data=json.dumps(data))

-     assert r.status_code == 200

-     old_decision = r.json()

-     second_msg = {

+     assert actual_msgs_sent[3] == {

          'policies_satisfied': True,

          'decision_context': 'bodhi_update_push_testing',

          'product_version': 'fedora-*',

          'unsatisfied_requirements': [],

          'summary': 'all required tests passed',

          'subject': [

-             {

-                 'item': nvr,

-                 'type': 'koji_build'

-             }

+             {'item': updateid, 'type': 'bodhi_update'},

+             {'item': nvr, 'type': 'koji_build'},

+             {'original_spec_nvr': nvr},

          ],

+         'subject_type': 'bodhi_update',

+         'subject_identifier': updateid,

          'applicable_policies': ['taskotron_release_critical_tasks_for_testing'],

-         'previous': old_decision,

+         'previous': {

+             'applicable_policies': ['taskotron_release_critical_tasks_for_testing'],

+             'policies_satisfied': False,

+             'summary': '1 of 1 required test results missing',

+             'unsatisfied_requirements': [

+                 {

+                     'testcase': 'dist.rpmdeplint',

+                     'item': {'item': nvr, 'type': 'koji_build'},

+                     'subject_type': 'koji_build',

+                     'subject_identifier': nvr,

+                     'type': 'test-result-missing',

+                     'scenario': None,

+                 },

+             ],

+         },

      }

-     mock_fedmsg.assert_any_call(topic='decision.update', msg=second_msg)

  

  

  @mock.patch('greenwave.consumers.resultsdb.fedmsg.config.load_config')
@@ -206,8 +320,7 @@ 

          #'topic_prefix.environment.waiver.new',

      ]

      handler.consume(message)

-     expected = ("greenwave.resources:retrieve_results|"

-                 "{u'item': u'%s', u'type': u'koji_build'}" % nvr)

+     expected = 'greenwave.resources:retrieve_results|koji_build %s' % nvr

      handler.cache.delete.assert_called_once_with(expected)

  

  
@@ -386,10 +499,14 @@ 

          u'policies_satisfied': False,

          'product_version': 'fedora-rawhide',

          'subject': [{u'productmd.compose.id': compose_id}],

+         'subject_type': 'compose',

+         'subject_identifier': compose_id,

          u'summary': u'1 of 2 required test results missing',

          'previous': old_decision,

          u'unsatisfied_requirements': [{

              u'item': {u'productmd.compose.id': compose_id},

+             'subject_type': 'compose',

+             'subject_identifier': compose_id,

              u'scenario': u'scenario2',

              u'testcase': u'compose.install_no_user',

              u'type': u'test-result-missing'}
@@ -465,6 +582,8 @@ 

                      'item': nvr,

                      'type': 'koji_build'

                  },

+                 'subject_type': 'koji_build',

+                 'subject_identifier': nvr,

                  'type': 'test-result-missing',

                  'scenario': None,

              },
@@ -474,6 +593,8 @@ 

                      'item': nvr,

                      'type': 'koji_build'

                  },

+                 'subject_type': 'koji_build',

+                 'subject_identifier': nvr,

                  'type': 'test-result-missing',

                  'scenario': None,

              }
@@ -485,6 +606,8 @@ 

                  'type': 'koji_build'

              }

          ],

+         'subject_type': 'koji_build',

+         'subject_identifier': nvr,

          'applicable_policies': ['taskotron_release_critical_tasks_with_blacklist',

                                  'taskotron_release_critical_tasks'],

          'previous': old_decision,
@@ -514,6 +637,8 @@ 

                  'type': 'koji_build'

              }

          ],

+         'subject_type': 'koji_build',

+         'subject_identifier': nvr,

          'applicable_policies': ['taskotron_release_critical_tasks_for_testing'],

          'previous': old_decision,

      }

@@ -1,7 +1,6 @@ 

  # SPDX-License-Identifier: GPL-2.0+

  

  import mock

- import json

  

  from greenwave.consumers import waiverdb

  
@@ -19,6 +18,8 @@ 

          mock_fedmsg, load_config, requests_session, greenwave_server, testdatabuilder):

      load_config.return_value = {'greenwave_api_url': greenwave_server + 'api/v1.0'}

      nvr = testdatabuilder.unique_nvr()

+     update = testdatabuilder.create_bodhi_update(build_nvrs=[nvr])

+     updateid = update['updateid']

      result = testdatabuilder.create_result(item=nvr,

                                             testcase_name='dist.abicheck',

                                             outcome='FAILED')
@@ -28,22 +29,14 @@ 

                                        testcase_name=testcase_name,

                                        outcome='PASSED')

      testcase = str(result['testcase']['name'])

-     waiver = testdatabuilder.create_waiver(result={

-         "subject": dict([(str(key), str(value[0])) for key, value in result['data'].items()]),

-         "testcase": testcase}, product_version='fedora-26', comment='Because I said so')

+     waiver = testdatabuilder.create_waiver(nvr=nvr,

+                                            testcase_name=testcase,

+                                            product_version='fedora-26',

+                                            comment='Because I said so')

      message = {

          'body': {

              'topic': 'waiver.new',

-             "msg": {

-                 "id": waiver['id'],

-                 "comment": "Because I said so",

-                 "username": "foo",

-                 "waived": "true",

-                 "timestamp": "2017-08-10T17:42:04.209638",

-                 "product_version": "fedora-26",

-                 "testcase": waiver['testcase'],

-                 "subject": [waiver['subject']]

-             }

+             "msg": waiver,

          }

      }

      hub = mock.MagicMock()
@@ -52,36 +45,69 @@ 

      assert handler.topic == ['topic_prefix.environment.waiver.new']

      handler.consume(message)

  

-     # get old decision

-     data = {

+     # We expect 2 messages altogether.

+     assert len(mock_fedmsg.mock_calls) == 2

+     assert all(call[2]['topic'] == 'decision.update' for call in mock_fedmsg.mock_calls)

+     actual_msgs_sent = [call[2]['msg'] for call in mock_fedmsg.mock_calls]

+     assert actual_msgs_sent[0] == {

+         'applicable_policies': ['taskotron_release_critical_tasks_with_blacklist',

+                                 'taskotron_release_critical_tasks'],

+         'policies_satisfied': True,

          'decision_context': 'bodhi_update_push_stable',

+         'previous': {

+             'applicable_policies': ['taskotron_release_critical_tasks_with_blacklist',

+                                     'taskotron_release_critical_tasks'],

+             'policies_satisfied': False,

+             'summary': u'1 of 3 required tests failed',

+             'unsatisfied_requirements': [

+                 {

+                     'result_id': result['id'],

+                     'item': {'item': nvr, 'type': 'koji_build'},

+                     'testcase': 'dist.abicheck',

+                     'type': 'test-result-failed',

+                     'scenario': None,

+                 },

+             ],

+         },

          'product_version': 'fedora-26',

-         'subject': [{'item': nvr, 'type': 'koji_build'}],

-         'ignore_waiver': [waiver['id']]

+         'subject': [

+             {'item': nvr, 'type': 'koji_build'},

+         ],

+         'subject_type': 'koji_build',

+         'subject_identifier': nvr,

+         'unsatisfied_requirements': [],

+         'summary': 'all required tests passed',

+         'testcase': testcase,

      }

-     r = requests_session.post(greenwave_server + 'api/v1.0/decision',

-                               headers={'Content-Type': 'application/json'},

-                               data=json.dumps(data))

-     assert r.status_code == 200

-     old_decision = r.json()

-     assert old_decision['summary'] == '1 of 3 required tests failed'

- 

-     msg = {

+     assert actual_msgs_sent[1] == {

          'applicable_policies': ['taskotron_release_critical_tasks_with_blacklist',

                                  'taskotron_release_critical_tasks'],

          'policies_satisfied': True,

          'decision_context': 'bodhi_update_push_stable',

-         'previous': old_decision,

+         'previous': {

+             'applicable_policies': ['taskotron_release_critical_tasks_with_blacklist',

+                                     'taskotron_release_critical_tasks'],

+             'policies_satisfied': False,

+             'summary': u'1 of 3 required tests failed',

+             'unsatisfied_requirements': [

+                 {

+                     'result_id': result['id'],

+                     'item': {'item': nvr, 'type': 'koji_build'},

+                     'testcase': 'dist.abicheck',

+                     'type': 'test-result-failed',

+                     'scenario': None,

+                 },

+             ],

+         },

          'product_version': 'fedora-26',

          'subject': [

-             {

-                 'item': nvr,

-                 'type': 'koji_build'

-             }

+             {'item': updateid, 'type': 'bodhi_update'},

+             {'item': nvr, 'type': 'koji_build'},

+             {'original_spec_nvr': nvr},

          ],

+         'subject_type': 'bodhi_update',

+         'subject_identifier': updateid,

          'unsatisfied_requirements': [],

          'summary': 'all required tests passed',

          'testcase': testcase,

      }

-     mock_fedmsg.assert_called_once_with(

-         topic='decision.update', msg=msg)

@@ -0,0 +1,62 @@ 

+ 

+ import re

+ import json

+ import hashlib

+ from urlparse import parse_qs

+ 

+ 

+ updates = {}  #: {id -> update info}

+ 

+ 

+ def application(environ, start_response):

+     path_info = environ['PATH_INFO']

+ 

+     m = re.match(r'/updates/(.+)$', path_info)

+     if m:

+         updateid = m.group(1)

+         if environ['REQUEST_METHOD'] == 'GET':

+             if updateid in updates:

+                 start_response('200 OK', [('Content-Type', 'application/json')])

+                 return [json.dumps({'update': updates[updateid]})]

+             else:

+                 start_response('404 Not Found', [])

+                 return []

+         else:

+             start_response('405 Method Not Allowed', [])

+             return []

+ 

+     m = re.match(r'/updates/$', path_info)

+     if m:

+         if environ['REQUEST_METHOD'] == 'GET':

+             params = parse_qs(environ['QUERY_STRING'])

+             if 'builds' in params:

+                 response_updates = [u for u in updates.values()

+                                     if set(params['builds']).issubset(build['nvr']

+                                                                       for build in u['builds'])]

+             else:

+                 response_updates = updates.values()

+             response_data = {

+                 'page': 1,

+                 'pages': 1,

+                 'rows_per_page': len(updates.values()),

+                 'total': len(updates.values()),

+                 'updates': response_updates,

+             }

+             start_response('200 OK', [('Content-Type', 'application/json')])

+             return [json.dumps(response_data)]

+         if environ['REQUEST_METHOD'] == 'POST':

+             body = environ['wsgi.input'].read(int(environ['CONTENT_LENGTH']))

+             updateid = 'FEDORA-2000-{}'.format(hashlib.sha1(body).hexdigest()[-8:])

+             assert updateid not in updates

+             update = json.loads(body)

+             update['updateid'] = updateid

+             updates[updateid] = update

+             print('Fake Bodhi created new update %r' % update)

+             start_response('201 Created', [('Content-Type', 'application/json')])

+             return [json.dumps(update)]  # XXX check what Bodhi really returns

+         else:

+             start_response('405 Method Not Allowed', [])

+             return []

+ 

+     start_response('404 Not Found', [])

+     return []

file modified
+115 -38
@@ -23,7 +23,7 @@ 

      assert r.status_code == 200

      body = r.json()

      policies = body['policies']

-     assert len(policies) == 6

+     assert len(policies) == 7

      assert any(p['id'] == 'taskotron_release_critical_tasks' for p in policies)

      assert any(p['decision_context'] == 'bodhi_update_push_stable' for p in policies)

      assert any(p['product_versions'] == ['fedora-26'] for p in policies)
@@ -59,7 +59,8 @@ 

  def test_cannot_make_decision_without_product_version(requests_session, greenwave_server):

      data = {

          'decision_context': 'bodhi_update_push_stable',

-         'subject': [{'item': 'foo-1.0.0-1.el7', 'type': 'koji_build'}]

+         'subject_type': 'bodhi_update',

+         'subject_identifier': 'FEDORA-2018-ec7cb4d5eb',

      }

      r = requests_session.post(greenwave_server + 'api/v1.0/decision',

                                headers={'Content-Type': 'application/json'},
@@ -71,7 +72,8 @@ 

  def test_cannot_make_decision_without_decision_context(requests_session, greenwave_server):

      data = {

          'product_version': 'fedora-26',

-         'subject': [{'item': 'foo-1.0.0-1.el7', 'type': 'koji_build'}]

+         'subject_type': 'bodhi_update',

+         'subject_identifier': 'FEDORA-2018-ec7cb4d5eb',

      }

      r = requests_session.post(greenwave_server + 'api/v1.0/decision',

                                headers={'Content-Type': 'application/json'},
@@ -80,16 +82,30 @@ 

      assert u'Missing required decision context' == r.json()['message']

  

  

- def test_cannot_make_decision_without_subject(requests_session, greenwave_server):

+ def test_cannot_make_decision_without_subject_type(requests_session, greenwave_server):

      data = {

          'decision_context': 'bodhi_update_push_stable',

          'product_version': 'fedora-26',

+         'subject_identifier': 'FEDORA-2018-ec7cb4d5eb',

      }

      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 u'Missing required subject' == r.json()['message']

+     assert u'Missing required "subject_type" parameter' == r.json()['message']

+ 

+ 

+ def test_cannot_make_decision_without_subject_identifier(requests_session, greenwave_server):

+     data = {

+         'decision_context': 'bodhi_update_push_stable',

+         'product_version': 'fedora-26',

+         'subject_type': 'bodhi_update',

+     }

+     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 u'Missing required "subject_identifier" parameter' == r.json()['message']

  

  

  def test_cannot_make_decision_with_invalid_subject(requests_session, greenwave_server):
@@ -102,7 +118,7 @@ 

                                headers={'Content-Type': 'application/json'},

                                data=json.dumps(data))

      assert r.status_code == 400

-     assert 'Invalid subject, must be a list of items' == r.json()['message']

+     assert 'Invalid subject, must be a list of dicts' == r.json()['message']

  

      data = {

          'decision_context': 'bodhi_update_push_stable',
@@ -113,34 +129,40 @@ 

                                headers={'Content-Type': 'application/json'},

                                data=json.dumps(data))

      assert r.status_code == 400

-     assert u'Invalid subject, must be a list of dicts' in r.text

+     assert 'Invalid subject, must be a list of dicts' == r.json()['message']

  

  

- def test_404_for_invalid_product_version(requests_session, greenwave_server):

+ def test_404_for_invalid_product_version(requests_session, greenwave_server, testdatabuilder):

+     update = testdatabuilder.create_bodhi_update(build_nvrs=[testdatabuilder.unique_nvr()])

      data = {

          'decision_context': 'bodhi_push_update_stable',

          'product_version': 'f26',  # not a real product version

-         'subject': [{'item': 'foo-1.0.0-1.el7', 'type': 'koji_build'}]

+         'subject_type': 'bodhi_update',

+         'subject_identifier': update['updateid'],

      }

      r = requests_session.post(greenwave_server + 'api/v1.0/decision',

                                headers={'Content-Type': 'application/json'},

                                data=json.dumps(data))

      assert r.status_code == 404

-     expected = u'Cannot find any applicable policies for f26 and bodhi_push_update_stable'

+     expected = (u'Cannot find any applicable policies for bodhi_update subjects '

+                 u'at gating point bodhi_push_update_stable in f26')

      assert expected == r.json()['message']

  

  

- def test_404_for_invalid_decision_context(requests_session, greenwave_server):

+ def test_404_for_invalid_decision_context(requests_session, greenwave_server, testdatabuilder):

+     update = testdatabuilder.create_bodhi_update(build_nvrs=[testdatabuilder.unique_nvr()])

      data = {

          'decision_context': 'bodhi_push_update',  # missing the _stable part!

          'product_version': 'fedora-26',

-         'subject': [{'item': 'foo-1.0.0-1.el7', 'type': 'koji_build'}]

+         'subject_type': 'bodhi_update',

+         'subject_identifier': update['updateid'],

      }

      r = requests_session.post(greenwave_server + 'api/v1.0/decision',

                                headers={'Content-Type': 'application/json'},

                                data=json.dumps(data))

      assert r.status_code == 404

-     expected = u'Cannot find any applicable policies for fedora-26 and bodhi_push_update'

+     expected = (u'Cannot find any applicable policies for bodhi_update subjects '

+                 u'at gating point bodhi_push_update in fedora-26')

      assert expected == r.json()['message']

  

  
@@ -166,10 +188,12 @@ 

          testdatabuilder.create_result(item=nvr,

                                        testcase_name=testcase_name,

                                        outcome='PASSED')

+     update = testdatabuilder.create_bodhi_update(build_nvrs=[nvr])

      data = {

          'decision_context': 'bodhi_update_push_stable',

          'product_version': 'fedora-26',

-         'subject': [{'item': nvr, 'type': 'koji_build'}]

+         'subject_type': 'bodhi_update',

+         'subject_identifier': update['updateid'],

      }

  

      r = requests_session.post(greenwave_server + 'api/v1.0/decision',
@@ -193,10 +217,12 @@ 

          results.append(testdatabuilder.create_result(item=nvr,

                                                       testcase_name=testcase_name,

                                                       outcome='PASSED'))

+     update = testdatabuilder.create_bodhi_update(build_nvrs=[nvr])

      data = {

          'decision_context': 'bodhi_update_push_stable',

          'product_version': 'fedora-26',

-         'subject': [{'item': nvr, 'type': 'koji_build'}],

+         'subject_type': 'bodhi_update',

+         'subject_identifier': update['updateid'],

          'verbose': True,

      }

  
@@ -216,22 +242,24 @@ 

          requests_session, greenwave_server, testdatabuilder):

      nvr = testdatabuilder.unique_nvr()

      # First one failed but was waived

-     result = testdatabuilder.create_result(item=nvr,

-                                            testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0],

-                                            outcome='FAILED')

-     waiver = testdatabuilder.create_waiver(result={ # noqa

-         "subject": dict([(key, value[0]) for key, value in result['data'].items()]),

-         "testcase": TASKTRON_RELEASE_CRITICAL_TASKS[0]}, product_version='fedora-26',

-         comment='This is fine')

+     testdatabuilder.create_result(item=nvr,

+                                   testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0],

+                                   outcome='FAILED')

+     testdatabuilder.create_waiver(nvr=nvr,

+                                   product_version='fedora-26',

+                                   testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0],

+                                   comment='This is fine')

      # The rest passed

      for testcase_name in TASKTRON_RELEASE_CRITICAL_TASKS[1:]:

          testdatabuilder.create_result(item=nvr,

                                        testcase_name=testcase_name,

                                        outcome='PASSED')

+     update = testdatabuilder.create_bodhi_update(build_nvrs=[nvr])

      data = {

          'decision_context': 'bodhi_update_push_stable',

          'product_version': 'fedora-26',

-         'subject': [{'item': nvr, 'type': 'koji_build'}]

+         'subject_type': 'bodhi_update',

+         'subject_identifier': update['updateid'],

      }

      r = requests_session.post(greenwave_server + 'api/v1.0/decision',

                                headers={'Content-Type': 'application/json'},
@@ -250,10 +278,12 @@ 

      result = testdatabuilder.create_result(item=nvr,

                                             testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0],

                                             outcome='FAILED')

+     update = testdatabuilder.create_bodhi_update(build_nvrs=[nvr])

      data = {

          'decision_context': 'bodhi_update_push_stable',

          'product_version': 'fedora-26',

-         'subject': [{'item': nvr, 'type': 'koji_build'}]

+         'subject_type': 'bodhi_update',

+         'subject_identifier': update['updateid'],

      }

      r = requests_session.post(greenwave_server + 'api/v1.0/decision',

                                headers={'Content-Type': 'application/json'},
@@ -276,6 +306,8 @@ 

      ] + [

          {

              'item': {'item': nvr, 'type': 'koji_build'},

+             'subject_type': 'koji_build',

+             'subject_identifier': nvr,

              'testcase': name,

              'type': 'test-result-missing',

              'scenario': None,
@@ -286,10 +318,12 @@ 

  

  def test_make_a_decision_on_no_results(requests_session, greenwave_server, testdatabuilder):

      nvr = testdatabuilder.unique_nvr()

+     update = testdatabuilder.create_bodhi_update(build_nvrs=[nvr])

      data = {

          'decision_context': 'bodhi_update_push_stable',

          'product_version': 'fedora-26',

-         'subject': [{'item': nvr, 'type': 'koji_build'}]

+         'subject_type': 'bodhi_update',

+         'subject_identifier': update['updateid'],

      }

      r = requests_session.post(greenwave_server + 'api/v1.0/decision',

                                headers={'Content-Type': 'application/json'},
@@ -304,6 +338,8 @@ 

      expected_unsatisfied_requirements = [

          {

              'item': {'item': nvr, 'type': 'koji_build'},

+             'subject_type': 'koji_build',

+             'subject_identifier': nvr,

              'testcase': name,

              'type': 'test-result-missing',

              'scenario': None,
@@ -315,10 +351,12 @@ 

  def test_empty_policy_is_always_satisfied(

          requests_session, greenwave_server, testdatabuilder):

      nvr = testdatabuilder.unique_nvr()

+     update = testdatabuilder.create_bodhi_update(build_nvrs=[nvr])

      data = {

          'decision_context': 'bodhi_update_push_stable',

          'product_version': 'fedora-24',

-         'subject': [{'item': nvr, 'type': 'koji_build'}]

+         'subject_type': 'bodhi_update',

+         'subject_identifier': update['updateid'],

      }

      r = requests_session.post(greenwave_server + 'api/v1.0/decision',

                                headers={'Content-Type': 'application/json'},
@@ -339,10 +377,12 @@ 

          testdatabuilder.create_result(item=nvr,

                                        testcase_name=testcase_name,

                                        outcome='PASSED')

+     update = testdatabuilder.create_bodhi_update(build_nvrs=[nvr])

      data = {

          'decision_context': 'bodhi_update_push_stable',

          'product_version': 'fedora-26',

-         'subject': [{'item': nvr, 'type': 'koji_build'}]

+         'subject_type': 'bodhi_update',

+         'subject_identifier': update['updateid'],

      }

      r = requests_session.post(greenwave_server + 'api/v1.0/decision',

                                headers={'Content-Type': 'application/json'},
@@ -376,10 +416,12 @@ 

          testdatabuilder.create_result(item=nvr,

                                        testcase_name=testcase_name,

                                        outcome='PASSED')

+     update = testdatabuilder.create_bodhi_update(build_nvrs=[nvr])

      data = {

          'decision_context': 'bodhi_update_push_stable',

          'product_version': 'fedora-26',

-         'subject': [{'item': nvr, 'type': 'koji_build'}]

+         'subject_type': 'bodhi_update',

+         'subject_identifier': update['updateid'],

      }

      r = requests_session.post(greenwave_server + 'api/v1.0/decision',

                                headers={'Content-Type': 'application/json'},
@@ -416,10 +458,12 @@ 

          testdatabuilder.create_result(item=nvr,

                                        testcase_name=testcase_name,

                                        outcome='PASSED')

+     update = testdatabuilder.create_bodhi_update(build_nvrs=[nvr])

      data = {

          'decision_context': 'bodhi_update_push_stable',

          'product_version': 'fedora-26',

-         'subject': [{'item': nvr, 'type': 'koji_build'}]

+         'subject_type': 'bodhi_update',

+         'subject_identifier': update['updateid'],

      }

      r = requests_session.post(greenwave_server + 'api/v1.0/decision',

                                headers={'Content-Type': 'application/json'},
@@ -437,6 +481,8 @@ 

      expected_unsatisfied_requirements = [

          {

              'item': {'item': nvr, 'type': 'koji_build'},

+             'subject_type': 'koji_build',

+             'subject_identifier': nvr,

              'testcase': TASKTRON_RELEASE_CRITICAL_TASKS[0],

              'type': 'test-result-missing',

              'scenario': None,
@@ -464,7 +510,8 @@ 

      data = {

          'decision_context': 'rawhide_compose_sync_to_mirrors',

          'product_version': 'fedora-rawhide',

-         'subject': [{'productmd.compose.id': compose_id}],

+         'subject_type': 'compose',

+         'subject_identifier': compose_id,

      }

      r = requests_session.post(greenwave_server + 'api/v1.0/decision',

                                headers={'Content-Type': 'application/json'},
@@ -500,7 +547,8 @@ 

      data = {

          'decision_context': 'rawhide_compose_sync_to_mirrors',

          'product_version': 'fedora-rawhide',

-         'subject': [{'productmd.compose.id': compose_id}],

+         'subject_type': 'compose',

+         'subject_identifier': compose_id,

      }

      r = requests_session.post(greenwave_server + 'api/v1.0/decision',

                                headers={'Content-Type': 'application/json'},
@@ -529,19 +577,21 @@ 

      result = testdatabuilder.create_result(item=nvr,

                                             testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0],

                                             outcome='FAILED')

-     waiver = testdatabuilder.create_waiver(result={

-         "subject": dict([(key, value[0]) for key, value in result['data'].items()]),

-         "testcase": TASKTRON_RELEASE_CRITICAL_TASKS[0]}, product_version='fedora-26',

-         comment='This is fine')

+     waiver = testdatabuilder.create_waiver(nvr=nvr,

+                                            testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0],

+                                            product_version='fedora-26',

+                                            comment='This is fine')

      # The rest passed

      for testcase_name in TASKTRON_RELEASE_CRITICAL_TASKS[1:]:

          testdatabuilder.create_result(item=nvr,

                                        testcase_name=testcase_name,

                                        outcome='PASSED')

+     update = testdatabuilder.create_bodhi_update(build_nvrs=[nvr])

      data = {

          'decision_context': 'bodhi_update_push_stable',

          'product_version': 'fedora-26',

-         'subject': [{'item': nvr, 'type': 'koji_build'}]

+         'subject_type': 'bodhi_update',

+         'subject_identifier': update['updateid'],

      }

      r_ = requests_session.post(greenwave_server + 'api/v1.0/decision',

                                 headers={'Content-Type': 'application/json'},
@@ -595,10 +645,12 @@ 

          testdatabuilder.create_result(item=nvr,

                                        testcase_name=testcase_name,

                                        outcome='PASSED')

+     update = testdatabuilder.create_bodhi_update(build_nvrs=[nvr])

      data = {

          'decision_context': 'bodhi_update_push_stable',

          'product_version': 'fedora-26',

-         'subject': [{'item': nvr, 'type': 'koji_build'}]

+         'subject_type': 'bodhi_update',

+         'subject_identifier': update['updateid'],

      }

      r = requests_session.post(greenwave_server + 'api/v1.0/decision',

                                headers={'Content-Type': 'application/json'},
@@ -632,10 +684,12 @@ 

          testdatabuilder.create_result(item=nvr,

                                        testcase_name=testcase_name,

                                        outcome='PASSED')

+     update = testdatabuilder.create_bodhi_update(build_nvrs=[nvr])

      data = {

          'decision_context': 'bodhi_update_push_stable',

          'product_version': 'fedora-26',

-         'subject': [{'item': nvr, 'type': 'koji_build'}]

+         'subject_type': 'bodhi_update',

+         'subject_identifier': update['updateid'],

      }

      r = requests_session.post(greenwave_server + 'api/v1.0/decision',

                                headers={'Content-Type': 'application/json'},
@@ -645,3 +699,26 @@ 

      # the failed test result of dist.abicheck should be ignored and thus the policy

      # is satisfied.

      assert res_data['policies_satisfied'] is True

+ 

+ 

+ def test_make_a_decision_about_brew_build(requests_session, greenwave_server, testdatabuilder):

+     # The 'brew-build' type is used internally within Red Hat. We treat it as

+     # the 'koji_build' subject type.

+     nvr = testdatabuilder.unique_nvr(name='avahi')

+     testdatabuilder.create_koji_build_result(

+         nvr=nvr, testcase_name='osci.brew-build.tier0.functional',

+         outcome='PASSED', type_='brew-build')

+     data = {

+         'decision_context': 'osci_compose_gate',

+         'product_version': 'rhel-something',

+         'subject': [{'type': 'brew-build', 'item': nvr}],

+     }

+ 

+     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 res_data['policies_satisfied'] is True

+     assert res_data['applicable_policies'] == ['osci_compose']

+     assert res_data['summary'] == 'all required tests passed'

file modified
+106 -41
@@ -4,12 +4,64 @@ 

  from werkzeug.exceptions import BadRequest, NotFound, UnsupportedMediaType, InternalServerError

  from greenwave import __version__

  from greenwave.policies import summarize_answers, RemoteOriginalSpecNvrRule

- from greenwave.resources import retrieve_results, retrieve_waivers

+ from greenwave.resources import retrieve_results, retrieve_waivers, retrieve_builds_in_update

  from greenwave.utils import insert_headers, jsonp

  

  api = (Blueprint('api_v1', __name__))

  

  

+ def subject_list_to_type_identifier(subject):

+     """

+     Greenwave < 0.8 accepted a list of arbitrary dicts for the 'subject'.

+     Now we expect a specific type and identifier.

+     This maps from the old style to the new, for backwards compatibility.

+ 

+     Note that WaiverDB has a very similar helper function, for compatibility

+     with WaiverDB < 0.11, but it accepts a single subject dict. Here we accept

+     a list.

+     """

+     if (not isinstance(subject, list) or not subject or

+             not all(isinstance(entry, dict) for entry in subject)):

+         raise BadRequest('Invalid subject, must be a list of dicts')

+     if any(entry.get('type') == 'bodhi_update' and 'item' in entry for entry in subject):

+         # Assume that all the other entries in the list are just for the

+         # builds which are in the Bodhi update. So really, the caller wants a

+         # decision about the Bodhi update. Ignore everything else. (Is this

+         # wise? Maybe not...)

+         identifier = [entry for entry in subject if entry.get('type') == 'bodhi_update'][0]['item']

+         return ('bodhi_update', identifier)

Go through all items at most once:

bodhi_update = next(
    (entry for entry in subject if entry.get('type') == 'bodhi_update' and 'item' in entry), None)
if bodhi_update is not None:
    return ('bodhi_update', bodhi_update['item'])

Oh yeah, not a bad suggestion, although I don't think it matters much since there will be at most 50 or so entries. I find the logic slightly easier to comprehend in my version. So I would prefer to leave it as is if you don't object.

+     if len(subject) == 1 and 'productmd.compose.id' in subject[0]:

+         return ('compose', subject[0]['productmd.compose.id'])

+     # We don't know of any callers who would ask about subjects like this,

+     # but it's easy enough to handle here anyway:

+     if len(subject) == 1 and subject[0].get('type') == 'brew-build' and 'item' in subject[0]:

+         return ('koji_build', subject[0]['item'])

+     if len(subject) == 1 and subject[0].get('type') == 'koji_build' and 'item' in subject[0]:

+         return ('koji_build', subject[0]['item'])

+     if len(subject) == 1 and 'original_spec_nvr' in subject[0]:

+         return ('koji_build', subject[0]['original_spec_nvr'])

+     raise BadRequest('Unrecognised subject type: %r' % subject)

+ 

+ 

+ def subject_type_identifier_to_list(subject_type, subject_identifier):

+     """

+     Inverse of the above function.

+     This is for backwards compatibility in emitted messages.

+     """

+     if subject_type == 'bodhi_update':

+         old_subject = [{'type': 'bodhi_update', 'item': subject_identifier}]

+         for nvr in retrieve_builds_in_update(subject_identifier):

+             old_subject.append({'type': 'koji_build', 'item': nvr})

+             old_subject.append({'original_spec_nvr': nvr})

+         return old_subject

+     elif subject_type == 'koji_build':

+         return [{'type': 'koji_build', 'item': subject_identifier}]

+     elif subject_type == 'compose':

+         return [{'productmd.compose.id': subject_identifier}]

+     else:

+         raise BadRequest('Unrecognised subject type: %s' % subject_type)

+ 

+ 

  @api.route('/version', methods=['GET'])

  @jsonp

  def version():
@@ -108,18 +160,14 @@ 

      .. sourcecode:: http

  

         POST /api/v1.0/decision HTTP/1.1

-        Host: localhost:5005

-        Accept-Encoding: gzip, deflate

         Accept: application/json

-        Connection: keep-alive

-        User-Agent: HTTPie/0.9.4

         Content-Type: application/json

-        Content-Length: 91

  

         {

             "decision_context": "bodhi_update_push_stable",

             "product_version": "fedora-26",

-            "subject": [{"item": "glibc-1.0-1.f26", "type": "koji_build"}],

+            "subject_type": "koji_build",

+            "subject_identifier": "cross-gcc-7.0.1-0.3.fc26",

             "verbose": true

         }

  
@@ -140,14 +188,13 @@ 

             "applicable_policies": ["1"],

             "unsatisfied_requirements": [

                 {

-                    'item': {"item": "glibc-1.0-1.f26", "type": "koji_build"},

                     'result_id': "123",

                     'testcase': 'dist.depcheck',

                     'type': 'test-result-failed'

                 },

                 {

-                    'item': {"item": "glibc-1.0-1.f26", "type": "koji_build"},

-                    'result_id': "124",

+                    "subject_type": "koji_build",

+                    "subject_identifier": "cross-gcc-7.0.1-0.3.fc26",

                     'testcase': 'dist.rpmlint',

                     'type': 'test-result-missing'

                 }
@@ -182,7 +229,8 @@ 

                   'waived': true,

                   'timestamp': '2018-01-23T18:02:04.630122',

                   'proxied_by': null,

-                  'subject': [{'item': 'cross-gcc-7.0.1-0.3.fc26', 'type': 'koji_build'}],

+                  "subject_type": "koji_build",

+                  "subject_identifier": "cross-gcc-7.0.1-0.3.fc26",

                   'testcase': 'dist.rpmlint',

                   'id': 1

                 }
@@ -193,9 +241,14 @@ 

      :jsonparam string decision_context: The decision context string, identified by a

          free-form string label. It is to be named through coordination between policy

          author and calling application, for example ``bodhi_update_push_stable``.

-     :jsonparam list subject: A list of items about which the caller is requesting a decision

-         used for querying ResultsDB. Each item contains one or more key-value pairs of 'data' key

-         in ResultsDB API. For example, [{"type": "koji_build", "item": "xscreensaver-5.37-3.fc27"}].

+     :jsonparam string subject_type: The type of software artefact we are

+         making a decision about, for example ``koji_build``.

+         See :ref:`subject-types` for a list of possible subject types.

+     :jsonparam string subject_identifier: A string identifying the software

+         artefact we are making a decision about. The meaning of the identifier

+         depends on the subject type.

+         See :ref:`subject-types` for details of how each subject type is identified.

+     :jsonparam list subject: *Deprecated:* Pass 'subject_type' and 'subject_identifier' instead.

It would make sense now to make /decision endpoint allow HTTP GET requests, if it's not difficult.

Agreed. We can add that after this.

      :jsonparam bool verbose: A flag to return additional information.

      :jsonparam list ignore_result: A list of result ids that will be ignored when making

          the decision.
@@ -212,14 +265,22 @@ 

          if ('decision_context' not in request.get_json() or

                  not request.get_json()['decision_context']):

              raise BadRequest('Missing required decision context')

-         if ('subject' not in request.get_json() or

-                 not request.get_json()['subject']):

-             raise BadRequest('Missing required subject')

      else:

          raise UnsupportedMediaType('No JSON payload in request')

      data = request.get_json()

-     if not isinstance(data['subject'], list):

-         raise BadRequest('Invalid subject, must be a list of items')

+ 

+     # Greenwave < 0.8

+     if 'subject' in data:

+         data['subject_type'], data['subject_identifier'] = \

+             subject_list_to_type_identifier(data['subject'])

+ 

+     if 'subject_type' not in data:

+         raise BadRequest('Missing required "subject_type" parameter')

+     if 'subject_identifier' not in data:

+         raise BadRequest('Missing required "subject_identifier" parameter')

+ 

+     subject_type = data['subject_type']

+     subject_identifier = data['subject_identifier']

      product_version = data['product_version']

      decision_context = data['decision_context']

      verbose = data.get('verbose', False)
@@ -239,32 +300,36 @@ 

                                                "'DIST_GIT_URL_TEMPLATE' and KOJI_BASE_URL in "

                                                "your configuration.")

  

-     applicable_policies = [policy for policy in current_app.config['policies']

-                            if policy.applies_to(decision_context, product_version)]

+     subject_policies = [policy for policy in current_app.config['policies']

+                         if policy.applies_to(decision_context, product_version, subject_type)]

+     if subject_type == 'bodhi_update':

+         # Also need to apply policies for each build in the update.

+         build_policies = [policy for policy in current_app.config['policies']

+                           if policy.applies_to(decision_context, product_version, 'koji_build')]

+     else:

+         build_policies = []

+     applicable_policies = subject_policies + build_policies

      if not applicable_policies:

          raise NotFound(

-             'Cannot find any applicable policies for %s and %s' % (

-                 product_version, decision_context))

-     subjects = [item for item in data['subject'] if isinstance(item, dict)]

-     if not subjects:

-         raise BadRequest('Invalid subject, must be a list of dicts')

+             'Cannot find any applicable policies for %s subjects at gating point %s in %s' % (

+                 subject_type, decision_context, product_version))

  

-     waivers = retrieve_waivers(product_version, subjects)

-     waivers = [w for w in waivers if w['id'] not in ignore_waivers]

- 

-     results = []

      answers = []

-     for item in subjects:

-         item_results = retrieve_results(item)

-         item_results = [r for r in item_results if r['id'] not in ignore_results]

-         results.extend(item_results)

- 

-         subject_subset = set(item.items())

-         item_waivers = [w for w in waivers if subject_subset <= set(w['subject'].items())]

- 

-         for policy in applicable_policies:

-             if policy.is_relevant_to(item):

-                 answers.extend(policy.check(item, item_results, item_waivers))

+     results = retrieve_results(subject_type, subject_identifier)

+     results = [r for r in results if r['id'] not in ignore_results]

+     waivers = retrieve_waivers(product_version, subject_type, subject_identifier)

+     waivers = [w for w in waivers if w['id'] not in ignore_waivers]

+     for policy in subject_policies:

+         answers.extend(policy.check(subject_identifier, results, waivers))

+     if build_policies:

+         build_nvrs = retrieve_builds_in_update(subject_identifier)

+         for nvr in build_nvrs:

+             results = retrieve_results('koji_build', nvr)

Looks like now it'll be possible to ask for multiple results (by using comma separated list of NVRs). I can look into this later and finally fix #117.

Oh yeah, you mean we can tweak retrieve_results() and retrieve_waivers() to take one subject type but multiple identifiers, right? And then each function can use the more efficient querying mechanisms in Resultsdb and Waiverdb to reduce the number of HTTP requests. Sounds like it will be a nice improvement.

+             results = [r for r in results if r['id'] not in ignore_results]

+             waivers = retrieve_waivers(product_version, 'koji_build', nvr)

+             waivers = [w for w in waivers if w['id'] not in ignore_waivers]

+             for policy in build_policies:

+                 answers.extend(policy.check(nvr, results, waivers))

  

      res = {

          'policies_satisfied': all(answer.is_satisfied for answer in answers),

file modified
+5 -7
@@ -16,6 +16,11 @@ 

  

      RESULTSDB_API_URL = 'https://taskotron.fedoraproject.org/resultsdb_api/api/v2.0'

      WAIVERDB_API_URL = 'https://waiverdb.fedoraproject.org/api/v1.0'

+     # Greenwave queries Bodhi to map builds <-> updates,

+     # so that it can make decisions about updates based on results for builds.

+     # If you don't have Bodhi, set this to None

+     # (this effectively disables the 'bodhi_update' subject type).

+     BODHI_URL = 'https://bodhi.fedoraproject.org/'

  

      # Options for outbound HTTP requests made by python-requests

      DIST_GIT_BASE_URL = 'https://src.fedoraproject.org'
@@ -33,13 +38,6 @@ 

      # By default, don't cache anything.

      CACHE = {'backend': 'dogpile.cache.null'}

  

-     # These are keys used to construct announcements about decision changes.

-     ANNOUNCEMENT_SUBJECT_KEYS = [

-         ('item', 'type',),

-         ('original_spec_nvr',),

-         ('productmd.compose.id',),

-     ]

- 

  

  class ProductionConfig(Config):

      DEBUG = False

@@ -19,6 +19,7 @@ 

  import greenwave.app_factory

  import greenwave.cache

  import greenwave.resources

+ from greenwave.api_v1 import subject_type_identifier_to_list

  

  requests_session = requests.Session()

  
@@ -60,10 +61,11 @@ 

  

      @staticmethod

      def announcement_subjects(message):

-         """ Yields subjects for announcement consideration from the message.

+         """

+         Yields pairs of (subject type, subject identifier) for announcement

+         consideration from the message.

  

          Args:

-             config (dict): The greenwave configuration.

              message (munch.Munch): A fedmsg about a new result.

          """

  
@@ -72,19 +74,30 @@ 

          except KeyError:

              data = message['msg']['task']  # Old format

  

-         announcement_keys = [

-             set(keys) for keys in current_app.config['ANNOUNCEMENT_SUBJECT_KEYS']

-         ]

- 

          def _decode(value):

              """ Decode either a string or a list of strings. """

              if len(value) == 1:

                  value = value[0]

              return value.decode('utf-8')

  

-         for keys in announcement_keys:

-             if keys.issubset(data.keys()):

-                 yield {key.decode('utf-8'): _decode(data[key]) for key in keys}

+         if data.get('type') == 'bodhi_update' and 'item' in data:

+             yield (u'bodhi_update', _decode(data['item']))

+         if 'productmd.compose.id' in data:

+             yield (u'compose', _decode(data['productmd.compose.id']))

+         if (data.get('type') == 'koji_build' and 'item' in data or

+                 data.get('type') == 'brew-build' and 'item' in data or

+                 'original_spec_nvr' in data):

+             if data.get('type') in ['koji_build', 'brew-build']:

+                 nvr = _decode(data['item'])

+             else:

+                 nvr = _decode(data['original_spec_nvr'])

+             yield (u'koji_build', nvr)

+             # If the result is for a build, it may also influence the decision

+             # about any update which the build is part of.

+             if current_app.config['BODHI_URL']:

+                 updateid = greenwave.resources.retrieve_update_for_build(nvr)

+                 if updateid is not None:

+                     yield (u'bodhi_update', updateid)

  

      def consume(self, message):

          """
@@ -107,12 +120,13 @@ 

              result_id = message['msg']['result']['id']  # Old format

  

          with self.flask_app.app_context():

-             for subject in self.announcement_subjects(message):

-                 log.debug('Considering subject "%s"', subject)

-                 self._invalidate_cache(subject)

-                 self._publish_decision_changes(subject, result_id, testcase)

+             for subject_type, subject_identifier in self.announcement_subjects(message):

+                 log.debug('Considering subject %s: %r', subject_type, subject_identifier)

+                 self._invalidate_cache(subject_type, subject_identifier)

+                 self._publish_decision_changes(subject_type, subject_identifier,

+                                                result_id, testcase)

  

-     def _publish_decision_changes(self, subject, result_id, testcase):

+     def _publish_decision_changes(self, subject_type, subject_identifier, result_id, testcase):

          """

          Process the given subject and publish a message if the decision is changed.

  
@@ -148,7 +162,8 @@ 

                  data = {

                      'decision_context': decision_context,

                      'product_version': product_version,

-                     'subject': [subject],

+                     'subject_type': subject_type,

+                     'subject_identifier': subject_identifier,

                  }

                  decision = greenwave.resources.retrieve_decision(greenwave_url, data)

  
@@ -160,7 +175,11 @@ 

  

                  if decision != old_decision:

                      decision.update({

-                         'subject': [subject],

+                         'subject_type': subject_type,

+                         'subject_identifier': subject_identifier,

+                         # subject is for backwards compatibility only:

+                         'subject': subject_type_identifier_to_list(subject_type,

+                                                                    subject_identifier),

                          'decision_context': decision_context,

                          'product_version': product_version,

                          'previous': old_decision,
@@ -169,16 +188,17 @@ 

                                'greenwave.decision.update')

                      fedmsg.publish(topic='decision.update', msg=decision)

  

-     def _invalidate_cache(self, subject):

+     def _invalidate_cache(self, subject_type, subject_identifier):

          """

          Process the given subject and delete cache keys as necessary.

  

          Args:

-             subject (munch.Munch): A subject argument, used to query greenwave.

+             subject_type (str): A subject type, used to query greenwave.

+             subject_identifier (str): A subject identifier, used to query greenwave.

          """

          namespace = None

          fn = greenwave.resources.retrieve_results

-         key = greenwave.cache.key_generator(namespace, fn)(subject)

+         key = greenwave.cache.key_generator(namespace, fn)(subject_type, subject_identifier)

          if not self.cache.get(key):

              log.debug("No cache value found for %r", key)

          else: