Repository: fedora-infra/bodhi

Issue:
Properly handle EPEL buildroot overrides
The epel8-next-build and epel9-next-build koji tags inherit packages from the epel8 and epel9 tags, respectively, but they don't inherit from epelX-override due to limitations in Koji (@nirik and I discussed this). However, when you create an override in Bodhi for a plain EPEL 8/9 package, Bodhi only tags the build into epelX-override. I propose either making this a special case and always tagging EPEL overrides into both epelX-override and epelX-next-override or allowing users to configure this.

This should *only* happen one way, though. Overrides for epelX packages should be tagged into both tags, but overrides for epelX-next packages should only be tagged into epelX-next-override.


Discussion:

Resolving patch:
diff --git a/bodhi-server/bodhi/server/models.py b/bodhi-server/bodhi/server/models.py
--- a/bodhi-server/bodhi/server/models.py
+++ b/bodhi-server/bodhi/server/models.py
@@ -1076,6 +1076,31 @@ def get_pending_signing_side_tag(self, from_tag: str) -> str:
             f'{self.setting_prefix}.koji-signing-pending-side-tag', "-signing-pending")
         return from_tag + side_tag_postfix
 
+    @property
+    def inherited_override_tags(self) -> list:
+        """
+        Return the override tags inherited from other Releases.
+
+        The override tags inheritance is configured by defining the list of
+        Releases we should extend from in the config file the same way of setting_prefix.
+        For example, if the release is epel9-n using epel-9.override-extend=EPEL-9N,EPEL-8
+        will make this method to return the override tag set for EPEL-9N and EPEL-8.
+
+        Note: pay attention, 'epel-9.override-extend' uses the release name in lowercase,
+        while the list of releases should match the release names case sensitive.
+
+        Returns:
+            A list of override tags inherited from other releases.
+        """
+        tags = []
+        inheritance = config.get(f'{self.setting_prefix}.override-extend', None)
+        if inheritance is not None:
+            rel_list = [Release.get(name.strip()) for name in inheritance.split(',')]
+            for rel in rel_list:
+                if rel is not None:
+                    tags.append(rel.override_tag)
+        return list(filter(None, tags))
+
 
 class TestCase(Base):
     """
@@ -3725,12 +3750,13 @@ def untag(self, db, preserve_override: bool = False):
         log.info("Untagging %s", self.alias)
         koji = buildsys.get_session()
         tag_types, tag_rels = Release.get_tags()
+        override_tags = self.release.inherited_override_tags + [self.release.override_tag]
         koji.multicall = True
         for build in self.builds:
             for tag in build.get_tags():
                 # Only remove tags that we know about
                 if tag in tag_rels:
-                    if preserve_override and tag == self.release.override_tag:
+                    if preserve_override and tag in override_tags:
                         log.info("Skipping override tag")
                     else:
                         koji.untagBuild(tag, build.nvr, force=True)
@@ -5060,7 +5086,8 @@ def edit(cls, request: 'pyramid.request', **data) -> 'BuildrootOverride':
     def enable(self) -> None:
         """Mark the BuildrootOverride as enabled."""
         koji_session = buildsys.get_session()
-        koji_session.tagBuild(self.build.release.override_tag, self.build.nvr)
+        for tag in self.build.release.inherited_override_tags + [self.build.release.override_tag]:
+            koji_session.tagBuild(tag, self.build.nvr)
 
         notifications.publish(override_schemas.BuildrootOverrideTagV1.from_dict(
             dict(override=self)))
@@ -5073,11 +5100,11 @@ def expire(self) -> None:
             return
 
         koji_session = buildsys.get_session()
-        try:
-            koji_session.untagBuild(self.build.release.override_tag,
-                                    self.build.nvr, strict=True)
-        except Exception as e:
-            log.error('Unable to untag override %s: %s' % (self.build.nvr, e))
+        for tag in self.build.release.inherited_override_tags + [self.build.release.override_tag]:
+            try:
+                koji_session.untagBuild(tag, self.build.nvr, strict=True)
+            except Exception as e:
+                log.error(f"Unable to untag override {self.build.nvr} from {tag}: '{e}'")
         self.expired_date = datetime.utcnow()
 
         notifications.publish(override_schemas.BuildrootOverrideUntagV1.from_dict(


Test patch:
diff --git a/bodhi-server/tests/test_models.py b/bodhi-server/tests/test_models.py
--- a/bodhi-server/tests/test_models.py
+++ b/bodhi-server/tests/test_models.py
@@ -858,6 +858,25 @@ def test_get_pending_testing_side_tag_not_found(self):
         """
         assert self.obj.get_pending_testing_side_tag("side-tag") == "side-tag-testing-pending"
 
+    def test_inherit_override_tags(self):
+        """
+        Assert that override tags are inherited from other releases.
+        """
+        self.create_release('17')
+        config.update({
+            'f11.override-extend': 'F17'
+        })
+        assert self.obj.inherited_override_tags == ['f17-override']
+
+    def test_inherit_override_tags_wrong_release_name(self):
+        """
+        Assert empty answer if user refer a non-existent release.
+        """
+        config.update({
+            'f11.override-extend': 'f17'
+        })
+        assert self.obj.inherited_override_tags == []
+
 
 class TestReleaseCritpathMinKarma(BasePyTestCase):
     """Tests for the Release.critpath_min_karma property."""
@@ -5049,7 +5068,8 @@ def test_expire_exception(self, error, get_session):
 
         get_session.return_value.untagBuild.assert_called_once_with(bro.build.release.override_tag,
                                                                     bro.build.nvr, strict=True)
-        error.assert_called_once_with('Unable to untag override {}: {}'.format(bro.nvr, 'oh no!'))
+        error.assert_called_once_with(f"Unable to untag override {bro.nvr} "
+                                      f"from {bro.build.release.override_tag}: 'oh no!'")
 
     def test_new_already_exists(self):
         """new() should put an error on the request if the BRO already exists."""
@@ -5064,3 +5084,37 @@ def test_new_already_exists(self):
         assert req.errors == (
             [{'location': 'body', 'name': 'nvr',
               'description': '{} is already in a override'.format(bro.build.nvr)}])
+
+    @mock.patch('bodhi.server.models.buildsys.get_session')
+    def test_override_with_inheritance(self, get_session):
+        """Build must be tagged/untagged in each inherited tag also."""
+        self.create_release('17')
+        config.update({
+            'f11.override-extend': 'F17'
+        })
+        package = model.RpmPackage(name='notbodhi')
+        self.db.add(package)
+        release_f17 = model.Release.get('F17')
+        release_f11 = model.Release.get('F11')
+        build = model.RpmBuild(nvr='notbodhi-1.0.0-1.fc11', release=release_f11,
+                               package=package, signed=True)
+        self.db.add(build)
+        self.db.commit()
+        user = model.User.query.first()
+        expiration_date = datetime.utcnow() + timedelta(days=1)
+        req = DummyRequest(user=user)
+        req.db = self.db
+
+        # Test tagging
+        model.BuildrootOverride.new(req, build=build, submitter=user,
+                                    expiration_date=expiration_date, notes='blabla')
+        calls = [mock.call(release_f17.override_tag, build.nvr),
+                 mock.call(build.release.override_tag, build.nvr)]
+        get_session.return_value.tagBuild.assert_has_calls(calls)
+
+        # Test untagging
+        bro = model.BuildrootOverride.get(build.id)
+        bro.expire()
+        calls = [mock.call(release_f17.override_tag, build.nvr, strict=True),
+                 mock.call(build.release.override_tag, build.nvr, strict=True)]
+        get_session.return_value.untagBuild.assert_has_calls(calls)