diff --git a/doc/conf.py b/doc/conf.py
index 67fb1615c..34488cddf 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -281,6 +281,31 @@
.. |se| raw:: html
+
+.. |br| raw:: html
+
+
+
+.. |p_red| raw:: html
+
+
+
+.. |p_end| raw:: html
+
+
+
+.. |p_yel| raw:: html
+
+
+
+.. |p_gre| raw:: html
+
+
+
+.. |p_gra| raw:: html
+
+
+
"""
# Documents to append as an appendix to all manuals.
diff --git a/doc/developer_documentation.rst b/doc/developer_documentation.rst
index 764eb3778..f9a53c96d 100644
--- a/doc/developer_documentation.rst
+++ b/doc/developer_documentation.rst
@@ -40,6 +40,7 @@ Misc
Brainstorming
Populate DB
+ Chroot EOL implementation
The design and diagrams
diff --git a/doc/developer_documentation/eol-logic.rst b/doc/developer_documentation/eol-logic.rst
new file mode 100644
index 000000000..67f300110
--- /dev/null
+++ b/doc/developer_documentation/eol-logic.rst
@@ -0,0 +1,48 @@
+.. _eol_logic:
+
+Handling EOL CoprChroot entries
+-------------------------------
+
+There are currently three cases when we schedule a CoprChroot for removal but
+preserve the data for some time to allow users to recover:
+
+ 1. User disables the chroot in a project ("unclicks" the checkbox). We give
+ them 14 days "preservation period" to reverse the decision without
+ forcing them to rebuild everything.
+ 2. Copr Admin makes a chroot EOL, e.g., `fedora-38-x86_64` because the
+ Fedora 38 version goes EOL. We keep the builds for several months, and
+ users can extend the preservation period.
+ 3. We disable rolling chroots (e.g., Fedora Rawhide or Fedora ELN) after a
+ reasonable period of inactivity.
+
+There are three database fields used for handling the EOL/preservation policies:
+``CoprChroot.deleted`` (bool), ``CoprChroot.delete_after`` (timestamp),
+and ``MockChroot.is_active`` (bool, 1:N mapped to ``CoprChroot``). The
+following table describes certain implications behind the logic:
+
+
+.. table:: Logical implications per in-DB chroot state
+
+
+ ========= ============ ======= ==================== ========= ===========
+ is_active delete_after deleted e-mail |br| can build State && |br| Description
+ notifications
+ ========= ============ ======= ==================== ========= ===========
+ yes yes yes no no |p_yel| ``preserved`` PM - preserved - manual removal |p_end|
+ yes no yes -- no |p_red| ``deleted`` manual removal or rolling removal (or EOL removal, and reactivated) |p_end|
+ yes yes no yes yes |p_yel| ``preserved`` rolling |p_end|
+ yes no no -- yes |p_gre| ``active`` normal chroot state |p_end|
+ no yes yes no no |p_yel| ``preserved`` (deleted manualy, then mock chroot EOL or deactivation) |p_end|
+ no no yes -- no |p_red| ``deleted`` manually OR rolling deleted, and THEN EOLed/deactivated by Copr admin |p_end|
+ no yes no yes no |p_yel| ``preserved`` mock chroot EOLed by Copr admin |p_end|
+ no no no -- no |p_gra| ``deactivated`` deactivated by Copr admin, data preserved |p_end|
+ ========= ============ ======= ==================== ========= ===========
+
+There's also a chroot state ``expired``, which is a special state of
+the ``preserved`` state. It is "still preserved", but the time for removal is
+already there, namely ``now() >= delete_after``.
+
+Note that when ``e-mail notifications`` are ``yes``, the time for removal has
+come (``now() >= delete_after``) and we **were not** able to send the
+notification e-mail, we **don't** remove the chroot data. **No unexpected
+removals.**
diff --git a/doc/how_to_manage_chroots.rst b/doc/how_to_manage_chroots.rst
index a27a3b0e2..49fc99e7d 100644
--- a/doc/how_to_manage_chroots.rst
+++ b/doc/how_to_manage_chroots.rst
@@ -20,6 +20,7 @@ Chroots can be easily managed with these few commands.
copr-frontend branch-fedora
copr-frontend rawhide-to-release
copr-frontend chroots-template [--template PATH]
+ copr-frontend eol-lifeless-rolling-chroots
However, `enablement process upon Fedora branching <#branching-process>`_ and also
`chroot deactivation when Fedora reaches it's EOL phase <#eol-deactivation-process>`_, are not that simple.
@@ -110,6 +111,22 @@ When it is done, `send an information email to a mailing list <#mailing-lists>`_
See the :ref:`the disable chroots template `.
+Rawhide (and other rolling) chroots EOL
+---------------------------------------
+
+Run ``copr-frontend eol-lifeless-rolling-chroots`` to mark existing rolling Copr
+chroots for the future removal/deactivation — if they appear lifeless. You
+might want to run this daily in the ``copr-frontend-optional`` cron-job file.
+The logic in this command checks that no build happened in particular rolling
+chroot for a long time, so likely no work is being done there, and the old built
+packages are _likely_ non-installable anyway (as the rolling distro moves
+forward with dependencies, but no dependency resolution is being done with
+RPMs). If such a chroot is marked EOL, this command applies the same
+notification policy/process as with the :ref:`eol_deactivation_process` so users
+can keep the chroot alive (either by prolonging the chroot, or triggering a new
+build).
+
+
.. _managing_chroot_comments:
Managing chroot comments
diff --git a/frontend/conf/cron.daily/copr-frontend-optional b/frontend/conf/cron.daily/copr-frontend-optional
index 714663c33..6cadb841a 100644
--- a/frontend/conf/cron.daily/copr-frontend-optional
+++ b/frontend/conf/cron.daily/copr-frontend-optional
@@ -2,6 +2,7 @@
# Optional Copr frontend tasks to be executed daily.
+#runuser -c 'copr-frontend eol-lifeless-rolling-chroots' - copr-fe
#runuser -c 'copr-frontend notify-outdated-chroots' - copr-fe
#runuser -c 'copr-frontend delete-outdated-chroots' - copr-fe
#/usr/libexec/copr_dump_db.sh /var/lib/copr/data/db_dumps/
diff --git a/frontend/coprs_frontend/alembic/versions/d23f84f87130_eol_rolling_builds.py b/frontend/coprs_frontend/alembic/versions/d23f84f87130_eol_rolling_builds.py
new file mode 100644
index 000000000..d72590079
--- /dev/null
+++ b/frontend/coprs_frontend/alembic/versions/d23f84f87130_eol_rolling_builds.py
@@ -0,0 +1,30 @@
+"""
+record last copr_chroot build, allow marking Rawhide as rolling
+Create Date: 2024-05-13 08:56:31.557843
+"""
+
+import time
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.sql import text
+
+revision = 'd23f84f87130'
+down_revision = '9fec2c962fcd'
+
+def upgrade():
+ op.add_column('mock_chroot', sa.Column('rolling', sa.Boolean(), nullable=True))
+ op.add_column('copr_chroot', sa.Column('last_build_timestamp', sa.Integer(), nullable=True))
+ conn = op.get_bind()
+ conn.execute(
+ text("update copr_chroot set last_build_timestamp = :start_stamp;"),
+ {"start_stamp": int(time.time())},
+ )
+ op.create_index('copr_chroot_rolling_last_build_idx', 'copr_chroot',
+ ['mock_chroot_id', 'last_build_timestamp', 'delete_after'],
+ unique=False)
+
+
+def downgrade():
+ op.drop_index('copr_chroot_rolling_last_build_idx', table_name='copr_chroot')
+ op.drop_column('copr_chroot', 'last_build_timestamp')
+ op.drop_column('mock_chroot', 'rolling')
diff --git a/frontend/coprs_frontend/commands/eol_lifeless_rolling_chroots.py b/frontend/coprs_frontend/commands/eol_lifeless_rolling_chroots.py
new file mode 100644
index 000000000..e484b2e51
--- /dev/null
+++ b/frontend/coprs_frontend/commands/eol_lifeless_rolling_chroots.py
@@ -0,0 +1,14 @@
+"""
+copr-frontend eol-lifeless-rolling-chroots command
+"""
+
+import click
+from coprs.logic.outdated_chroots_logic import OutdatedChrootsLogic
+
+@click.command()
+def eol_lifeless_rolling_chroots():
+ """
+ Go through all rolling CoprChroots and check whether they shouldn't be
+ scheduled for future removal.
+ """
+ OutdatedChrootsLogic.trigger_rolling_eol_policy()
diff --git a/frontend/coprs_frontend/config/copr.conf b/frontend/coprs_frontend/config/copr.conf
index 625ed0050..957a99014 100644
--- a/frontend/coprs_frontend/config/copr.conf
+++ b/frontend/coprs_frontend/config/copr.conf
@@ -207,6 +207,15 @@ HIDE_IMPORT_LOG_AFTER_DAYS = 14
# failed builds. Currently only integrated with the Fedora Copr instance.
#LOG_DETECTIVE_BUTTON = False
+# After a given _INACTIVITY_WARNING period (DAYS), when no new build appeared
+# new build appeared in a rolling chroot, we start the EOL policy. It means
+# that - after the next _INACTIVITY_REMOVAL DAYS - the chroot gets disabled, and
+# builds the corresponding builds removed to save storage. This is only
+# applicable to MockChroots.rolling = True, and the ... TODO ... command must be
+# configured in cron.
+#ROLLING_CHROOTS_INACTIVITY_WARNING = 180
+#ROLLING_CHROOTS_INACTIVITY_REMOVAL = 180
+
#############################
##### DEBUGGING Section #####
diff --git a/frontend/coprs_frontend/coprs/config.py b/frontend/coprs_frontend/coprs/config.py
index 296ad081c..9797d43c8 100644
--- a/frontend/coprs_frontend/coprs/config.py
+++ b/frontend/coprs_frontend/coprs/config.py
@@ -198,6 +198,10 @@ class Config(object):
LOG_DETECTIVE_BUTTON = False
+ ROLLING_CHROOTS_INACTIVITY_WARNING = 180
+ ROLLING_CHROOTS_INACTIVITY_REMOVAL = 180
+
+
class ProductionConfig(Config):
DEBUG = False
# SECRET_KEY = "put_some_secret_here"
diff --git a/frontend/coprs_frontend/coprs/helpers.py b/frontend/coprs_frontend/coprs/helpers.py
index 94ccd5d6d..c6645eab2 100644
--- a/frontend/coprs_frontend/coprs/helpers.py
+++ b/frontend/coprs_frontend/coprs/helpers.py
@@ -202,6 +202,8 @@ class ChrootDeletionStatus(metaclass=EnumType):
When a chroot is marked as EOL or when it is unclicked from a project,
it goes through several stages before its data is finally deleted.
Each `models.CoprChroot` is in one of the following states.
+
+ See https://docs.pagure.org/copr.copr/developer_documentation/eol-logic.html
"""
# pylint: disable=too-few-public-methods
vals = {
@@ -213,14 +215,10 @@ class ChrootDeletionStatus(metaclass=EnumType):
# marked as EOL and its data is never going to be deleted.
"deactivated": 1,
- # There are multiple possible scenarios for chroots in this state:
- # 1) The standard preservation period is not over yet. Its length
- # differs on whether the chroot is EOL or was unclicked from
- # a project but the meaning is same for both cases
- #
- # 2) If the chroot is EOL and we wasn't able to send a notification
- # about it.
- #
+ # Preserved state. There are multiple possible scenarios for chroots in
+ # this state:
+ # 1) The standard preservation period is not over yet.
+ # 2) If the chroot is EOL, but we weren't able to notify the user.
# 3) Any other constraint that disallows the chroot to be deleted yet.
# At this moment there shouldn't be any.
"preserved": 2,
diff --git a/frontend/coprs_frontend/coprs/logic/actions_logic.py b/frontend/coprs_frontend/coprs/logic/actions_logic.py
index c86233c67..791bd751f 100644
--- a/frontend/coprs_frontend/coprs/logic/actions_logic.py
+++ b/frontend/coprs_frontend/coprs/logic/actions_logic.py
@@ -398,6 +398,7 @@ def send_delete_chroot(cls, copr_chroot):
created_on=int(time.time()),
copr_id=copr_chroot.copr.id,
)
+ copr_chroot.deleted = True
db.session.add(action)
return action
diff --git a/frontend/coprs_frontend/coprs/logic/builds_logic.py b/frontend/coprs_frontend/coprs/logic/builds_logic.py
index 825cad364..509136e50 100644
--- a/frontend/coprs_frontend/coprs/logic/builds_logic.py
+++ b/frontend/coprs_frontend/coprs/logic/builds_logic.py
@@ -1473,6 +1473,7 @@ def new(cls, build, mock_chroot, **kwargs):
copr_chroot = coprs_logic.CoprChrootsLogic.get_by_mock_chroot_id(
build.copr, mock_chroot.id
).one()
+ copr_chroot.build_done()
return models.BuildChroot(
mock_chroot=mock_chroot,
copr_chroot=copr_chroot,
diff --git a/frontend/coprs_frontend/coprs/logic/complex_logic.py b/frontend/coprs_frontend/coprs/logic/complex_logic.py
index 0b65d8987..9bdf449bf 100644
--- a/frontend/coprs_frontend/coprs/logic/complex_logic.py
+++ b/frontend/coprs_frontend/coprs/logic/complex_logic.py
@@ -785,6 +785,7 @@ def _delete_reason(cls, copr_chroots):
"""
In case some of the `copr_chroots` is going to be deleted in the future,
describe the reason why
+ TODO: merged with the `CoprChroot.delete_status` property
"""
# Do we even want to show a trash icon? I.e. are any of the project
# chroots set to be deleted in the future?
@@ -798,13 +799,21 @@ def _delete_reason(cls, copr_chroots):
# project owner. Or all of them for the same reason. Also all
# architectures may be deleted by the project owner but on a different
# day and therefore the remaining time may be different)
+ # See https://docs.pagure.org/copr.copr/developer_documentation/eol-logic.html
reasons = {}
for chroot in delete_chroots:
reason_format = "{0} and will remain available for another {1} days"
- reason = reason_format.format(
- "disabled by a project owner" if chroot.is_active else "EOL",
- chroot.delete_after_days,
- )
+ reason = "EOL"
+ if chroot.is_active:
+ if chroot.deleted:
+ # This is in `preserved` state, not `deleted`, otherwise we
+ # wouldn't even ask for the _delete_reason().
+ reason = "disabled by a project owner"
+ elif chroot.mock_chroot.rolling:
+ reason = "a rolling chroot inactive for a long time"
+ else:
+ raise exceptions.BadRequest(f"Unknown EOL reason {chroot.name}")
+ reason = reason_format.format(reason, chroot.delete_after_days)
reasons.setdefault(reason, [])
reasons[reason].append(chroot.mock_chroot.arch)
diff --git a/frontend/coprs_frontend/coprs/logic/coprs_logic.py b/frontend/coprs_frontend/coprs/logic/coprs_logic.py
index 8e7c0b460..17c00ec70 100644
--- a/frontend/coprs_frontend/coprs/logic/coprs_logic.py
+++ b/frontend/coprs_frontend/coprs/logic/coprs_logic.py
@@ -8,7 +8,7 @@
import flask
-from sqlalchemy import not_
+from sqlalchemy import not_, or_
from sqlalchemy import desc
from sqlalchemy import func
from sqlalchemy.event import listens_for
@@ -1251,18 +1251,22 @@ def remove_copr_chroot(cls, user, copr_chroot):
@classmethod
def filter_outdated(cls, query):
"""
- Filter query to fetch only `CoprChroot` instances that are EOL but still
- in the data preservation period
+ Filter query to fetch only `CoprChroot` instances that are in the data
+ preservation period, but not yet expired (not yet to be removed). Used
+ for sending e-mail notifications.
+
+ See https://docs.pagure.org/copr.copr/developer_documentation/eol-logic.html
"""
return (query.filter(models.CoprChroot.delete_after
>= datetime.datetime.now())
- # Filter only such chroots that are not unclicked (deleted)
- # from a project. We don't want the EOL machinery for them,
- # they are deleted.
+ # Filter out chroots that are manually disabled by user
+ # (deleted, aka "unclicked", we never send e-mails there)
.filter(models.CoprChroot.deleted.isnot(True))
-
- # Filter only inactive (i.e. EOL) chroots
- .filter(not_(models.MockChroot.is_active)))
+ .filter(or_(
+ # inactive (i.e. EOL) chroots
+ not_(models.MockChroot.is_active),
+ # rolling EOL candidate (still active, though)
+ models.MockChroot.rolling.is_(True))))
@classmethod
diff --git a/frontend/coprs_frontend/coprs/logic/outdated_chroots_logic.py b/frontend/coprs_frontend/coprs/logic/outdated_chroots_logic.py
index a524fabdd..01f8a65a3 100644
--- a/frontend/coprs_frontend/coprs/logic/outdated_chroots_logic.py
+++ b/frontend/coprs_frontend/coprs/logic/outdated_chroots_logic.py
@@ -83,10 +83,43 @@ def expire(cls, copr_chroot):
cls._update_copr_chroot(copr_chroot, delete_after_days)
@classmethod
- def _update_copr_chroot(cls, copr_chroot, delete_after_days):
+ def _update_copr_chroot(cls, copr_chroot, delete_after_days, actor=None):
delete_after_timestamp = (
datetime.now()
+ timedelta(days=delete_after_days)
)
- CoprChrootsLogic.update_chroot(flask.g.user, copr_chroot,
+
+ if actor is None:
+ actor = flask.g.user
+
+ CoprChrootsLogic.update_chroot(actor, copr_chroot,
delete_after=delete_after_timestamp)
+
+
+ @classmethod
+ def trigger_rolling_eol_policy(cls):
+ """
+ Go through all the MockChroot.rolling -> CoprChroots, and check when the
+ last build has been done. If it is more than
+ config.ROLLING_CHROOTS_INACTIVITY_WARNING days, mark the CoprChroot for
+ removal after ROLLING_CHROOTS_INACTIVITY_REMOVAL days.
+ """
+
+ period = app.config["ROLLING_CHROOTS_INACTIVITY_WARNING"] * 24 * 3600
+ warn_timestamp = int(datetime.now().timestamp()) - period
+
+ query = (
+ db.session.query(models.CoprChroot).join(models.MockChroot)
+ .filter(models.MockChroot.rolling.is_(True))
+ .filter(models.MockChroot.is_active.is_(True))
+ .filter(models.CoprChroot.delete_after.is_(None))
+ .filter(models.CoprChroot.last_build_timestamp.isnot(None))
+ .filter(models.CoprChroot.last_build_timestamp < warn_timestamp)
+ )
+
+ when = app.config["ROLLING_CHROOTS_INACTIVITY_REMOVAL"]
+ for chroot in query:
+ cls._update_copr_chroot(chroot, when,
+ models.AutomationUser("Rolling-Builds-Cleaner"))
+
+ db.session.commit()
diff --git a/frontend/coprs_frontend/coprs/models.py b/frontend/coprs_frontend/coprs/models.py
index b247bf8f2..d41827ce4 100644
--- a/frontend/coprs_frontend/coprs/models.py
+++ b/frontend/coprs_frontend/coprs/models.py
@@ -104,13 +104,20 @@ def can_edit(self, copr, ignore_admin=False):
"""
raise NotImplementedError
+ @property
+ def name(self):
+ """
+ Return the short username of the user, e.g. bkabrda
+ """
+ return self.username
+
class AutomationUser(AbstractUser):
"""
This user (instance of this class) can modify all projects; used for
internal system operations like automatic build removals, etc.
"""
- def can_edit(self, _copr, _ignore_admin=False):
+ def can_edit(self, _copr, ignore_admin=False):
return True
def __init__(self, name):
@@ -127,13 +134,6 @@ class User(db.Model, helpers.Serializer, AbstractUser):
__table__ = outerjoin(_UserPublic.__table__, _UserPrivate.__table__)
id = column_property(_UserPublic.__table__.c.id, _UserPrivate.__table__.c.user_id)
- @property
- def name(self):
- """
- Return the short username of the user, e.g. bkabrda
- """
-
- return self.username
@property
def copr_permissions(self):
@@ -1629,6 +1629,9 @@ class MockChroot(db.Model, TagMixin, helpers.Serializer):
comment = db.Column(db.Text, nullable=True)
+ # rolling distribution, e.g. Fedora Rawhide
+ rolling = db.Column(db.Boolean, default=False)
+
multilib_pairs = {
'x86_64': 'i386',
}
@@ -1702,6 +1705,8 @@ class CoprChroot(db.Model, helpers.Serializer):
# slightly different configuration).
db.UniqueConstraint("mock_chroot_id", "copr_id",
name="copr_chroot_mock_chroot_id_copr_id_uniq"),
+ db.Index("copr_chroot_rolling_last_build_idx",
+ "mock_chroot_id", "last_build_timestamp", "delete_after"),
)
copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"))
@@ -1741,6 +1746,8 @@ class CoprChroot(db.Model, helpers.Serializer):
isolation = db.Column(db.Text, default="unchanged")
deleted = db.Column(db.Boolean, default=False, index=True)
+ last_build_timestamp = db.Column(db.Integer)
+
def update_comps(self, comps_xml):
"""
save (compressed) the comps_xml file content (instance of bytes).
@@ -1787,6 +1794,8 @@ def delete_status(self):
The WTF/minute ratio for reading this method is way above bearable level
but we are considering 5 boolean variables and basically doing 2^5
binary table.
+
+ https://docs.pagure.org/copr.copr/developer_documentation/eol-logic.html
"""
# pylint: disable=too-many-return-statements
@@ -1800,7 +1809,7 @@ def delete_status(self):
return ChrootDeletionStatus("preserved")
- # Chroots that we deactivated or marked as EOL
+ # Chroots that Copr admins deactivated or marked as EOL
if not self.is_active:
# This chroot is not EOL, its just _temporarily_ deactivated
if not self.delete_after and not self.delete_notify:
@@ -1819,9 +1828,20 @@ def delete_status(self):
return ChrootDeletionStatus("preserved")
+ # Normal enabled && active chroots
if not self.delete_after and not self.delete_notify:
return ChrootDeletionStatus("active")
+ # EOLed rolling chroots (reactivate by a new build)
+ if self.delete_after and self.mock_chroot.rolling:
+ # We can never ever remove EOL chroots that we didn't send
+ # a notification about
+ if not self.delete_notify:
+ return ChrootDeletionStatus("preserved")
+ if self.delete_after < datetime.datetime.now():
+ return ChrootDeletionStatus("expired")
+ return ChrootDeletionStatus("preserved")
+
raise RuntimeError("Undefined status, this shouldn't happen")
@property
@@ -1921,6 +1941,14 @@ def isolation_setup(self):
return {}
return settings
+ def build_done(self):
+ """
+ Record that a build has been submitted into this CoprChroot.
+ """
+ self.delete_after = None
+ self.last_build_timestamp = int(time.time())
+ self.delete_notify = None
+
class BuildChroot(db.Model, TagMixin, helpers.Serializer):
"""
diff --git a/frontend/coprs_frontend/manage.py b/frontend/coprs_frontend/manage.py
index 689cce9f6..ed0c8f06f 100755
--- a/frontend/coprs_frontend/manage.py
+++ b/frontend/coprs_frontend/manage.py
@@ -35,6 +35,7 @@
import commands.vacuum_graphs
import commands.notify_outdated_chroots
import commands.delete_outdated_chroots
+import commands.eol_lifeless_rolling_chroots
import commands.clean_expired_projects
import commands.clean_old_builds
import commands.delete_orphans
@@ -89,6 +90,7 @@
"vacuum_graphs",
"notify_outdated_chroots",
"delete_outdated_chroots",
+ "eol_lifeless_rolling_chroots",
"clean_expired_projects",
"clean_old_builds",
"delete_orphans",
diff --git a/frontend/coprs_frontend/tests/coprs_test_case.py b/frontend/coprs_frontend/tests/coprs_test_case.py
index b4142c269..6a5e1a077 100644
--- a/frontend/coprs_frontend/tests/coprs_test_case.py
+++ b/frontend/coprs_frontend/tests/coprs_test_case.py
@@ -217,7 +217,8 @@ def f_mock_chroots(self):
self.mc3.distgit_branch = self.mc2.distgit_branch
self.mc4 = models.MockChroot(
- os_release="fedora", os_version="rawhide", arch="i386", is_active=True)
+ os_release="fedora", os_version="rawhide", arch="i386",
+ is_active=True, rolling=True)
self.mc4.distgit_branch = models.DistGitBranch(name='master')
self.mc_basic_list = [self.mc1, self.mc2, self.mc3, self.mc4]
diff --git a/frontend/coprs_frontend/tests/test_logic/test_coprs_logic.py b/frontend/coprs_frontend/tests/test_logic/test_coprs_logic.py
index 5d8ec81ef..ab8cd9d8d 100644
--- a/frontend/coprs_frontend/tests/test_logic/test_coprs_logic.py
+++ b/frontend/coprs_frontend/tests/test_logic/test_coprs_logic.py
@@ -212,9 +212,17 @@ def test_filter_outdated(self, f_users, f_coprs, f_mock_chroots, f_db):
# A chroot is not EOL but was unclicked from a project by its owner
self.c2.copr_chroots[0].mock_chroot.is_active = True
+ self.c2.copr_chroots[0].deleted = True
self.c2.copr_chroots[0].delete_after = datetime.today() + timedelta(days=1)
assert outdated.all() == []
+ # A rolling chroot is inactive
+ self.c2.copr_chroots[0].mock_chroot.is_active = True
+ self.c2.copr_chroots[0].deleted = False
+ self.c2.copr_chroots[0].delete_after = datetime.today() + timedelta(days=1)
+ assert len(outdated.all()) == 1
+
+
def test_filter_outdated_to_be_deleted(self, f_users, f_coprs, f_mock_chroots, f_db):
outdated = CoprChrootsLogic.filter_to_be_deleted(CoprChrootsLogic.get_multiple())
assert outdated.all() == []
diff --git a/frontend/coprs_frontend/tests/test_logic/test_outdated_chroots_logic.py b/frontend/coprs_frontend/tests/test_logic/test_outdated_chroots_logic.py
index 954fc1fa9..522d79724 100644
--- a/frontend/coprs_frontend/tests/test_logic/test_outdated_chroots_logic.py
+++ b/frontend/coprs_frontend/tests/test_logic/test_outdated_chroots_logic.py
@@ -377,3 +377,18 @@ def test_outdated_unclicked_repeat(self):
CoprChrootsLogic.update_from_names(
self.u2, self.c2, [chroot.name for chroot in self.c2.copr_chroots])
assert not self.c2.copr_chroots[0].delete_after
+
+ @pytest.mark.usefixtures("f_users", "f_coprs", "f_mock_chroots", "f_db")
+ def test_rolling_warning(self):
+ for cc in self.models.CoprChroot.query.all():
+ if cc.copr.full_name == "user2/barcopr" and cc.name == "fedora-rawhide-i386":
+ # distant past!
+ cc.last_build_timestamp = 666
+ self.db.session.commit()
+ OutdatedChrootsLogic.trigger_rolling_eol_policy()
+ eols = self.db.session.query(self.models.CoprChroot)\
+ .filter(self.models.CoprChroot.delete_after.isnot(None)).all()
+
+ assert len(eols) == 1
+ assert eols[0].name == "fedora-rawhide-i386"
+ assert eols[0].delete_after_days == app.config["ROLLING_CHROOTS_INACTIVITY_REMOVAL"]-1