Fix manual Status Update runs with multiple candidate integration configs.

Review Request #12835 — Created Feb. 21, 2023 and submitted — Latest diff uploaded

Information

Review Board
release-5.0.x

Reviewers

When performing a manual run of a StatusUpdate, the code responding to
the manual run signal had no choice but to try to find a matching
IntegrationConfig for the review request. If multiple matches were
found, it had to pick one, and this could easily be the wrong one.

To solve this, we now store the configuration ID in
StatusUpdate.extra_data, using a private key. This is stored either
when setting the new StatusUpdate.integration_config property, or when
creating the StatusUpdate using
StatusUpdate.objects.create_for_integration() (which is the preferred
way going forward).

When either setting or fetching the configuration, the configuration is
validated to make sure it matches the same LocalSite, to avoid any
cross-site contamination issues.

The status_update_request_run signal is now given a config argument
mapping to this stored configuration (if one is stored), which
integrations can use. This will require a change for integrations
handling that signal to prioritize the new argument.

Old Status Updates in manual run mode will result in a None
configuration, which will need to be handled through legacy means, but
all future Status Updates created after this change will have a suitable
configuration.

All unit tests pass.

Tested this along with changes in rbintegrations to make use of the
new config state.

This will be tested in production with an affected customer.

Diff Revision 2 (Latest)

orig
1
2

Commits

First Last Summary ID Author
Fix manual Status Update runs with multiple candidate integration configs.
When performing a manual run of a `StatusUpdate`, the code responding to the manual run signal had no choice but to try to find a matching `IntegrationConfig` for the review request. If multiple matches were found, it had to pick one, and this could easily be the wrong one. To solve this, we now store the configuration ID in `StatusUpdate.extra_data`, using a private key. This is stored either when setting the new `StatusUpdate.integration_config` property, or when creating the `StatusUpdate` using `StatusUpdate.objects.create_for_integration()` (which is the preferred way going forward). When either setting or fetching the configuration, the configuration is validated to make sure it matches the same `LocalSite`, to avoid any cross-site contamination issues. The `status_update_request_run` signal is now given a `config` argument mapping to this stored configuration (if one is stored), which integrations can use. This will require a change for integrations handling that signal to prioritize the new argument. Old Status Updates in manual run mode will result in a `None` configuration, which will need to be handled through legacy means, but all future Status Updates created after this change will have a suitable configuration.
0e6fcb14583551c29b9a546448c6c8b678369200 Christian Hammond
reviewboard/reviews/managers.py
Revision b0bc89dfac9ee6ab49dfe3cd3eb37fdc0a521f3c New Change
1
"""Managers for reviewboard.reviews.models."""
1
"""Managers for reviewboard.reviews.models."""
2

    
   
2

   

    
   
3
from __future__ import annotations

    
   
4

   
3
import logging
5
import logging

    
   
6
from typing import Dict, Optional, TYPE_CHECKING
4

    
   
7

   
5
from django.contrib.auth.models import AnonymousUser, User
8
from django.contrib.auth.models import AnonymousUser, User
6
from django.core.exceptions import ObjectDoesNotExist
9
from django.core.exceptions import ObjectDoesNotExist
7
from django.db import connections, router, transaction, IntegrityError
10
from django.db import connections, router, transaction, IntegrityError
8
from django.db.models import Manager, Q
11
from django.db.models import Manager, Q
9
from django.db.models.query import QuerySet
12
from django.db.models.query import QuerySet

    
   
13
from django.utils.text import slugify
10
from djblets.db.managers import ConcurrencyManager
14
from djblets.db.managers import ConcurrencyManager
11

    
   
15

   
12
from reviewboard.deprecation import RemovedInReviewBoard60Warning
16
from reviewboard.deprecation import RemovedInReviewBoard60Warning
13
from reviewboard.diffviewer.models import DiffSetHistory
17
from reviewboard.diffviewer.models import DiffSetHistory
14
from reviewboard.scmtools.errors import ChangeNumberInUseError
18
from reviewboard.scmtools.errors import ChangeNumberInUseError
15
from reviewboard.scmtools.models import Repository
19
from reviewboard.scmtools.models import Repository
16
from reviewboard.site.models import LocalSite
20
from reviewboard.site.models import LocalSite
17

    
   
21

   

    
   
22
if TYPE_CHECKING:

    
   
23
    from reviewboard.changedescs.models import ChangeDescription

    
   
24
    from reviewboard.integrations.base import Integration

    
   
25
    from reviewboard.integrations.models import IntegrationConfig

    
   
26
    from reviewboard.reviews.models import ReviewRequest, StatusUpdate

    
   
27

   
18

    
   
28

   
19
logger = logging.getLogger(__name__)
29
logger = logging.getLogger(__name__)
20

    
   
30

   
21

    
   
31

   
22
class CommentManager(ConcurrencyManager):
32
class CommentManager(ConcurrencyManager):
1539 lines
def _query(self, user=None, public=None, status='P', extra_query=None,
1562

    
   
1572

   
1563
        if distinct:
1573
        if distinct:
1564
            queryset = queryset.distinct()
1574
            queryset = queryset.distinct()
1565

    
   
1575

   
1566
        return queryset
1576
        return queryset

    
   
1577

   

    
   
1578

   

    
   
1579
class StatusUpdateManager(Manager):

    
   
1580
    """A manager for StatusUpdate models.

    
   
1581

   

    
   
1582
    This offers conveniences around creating

    
   
1583
    :py:class:`~reviewboard.reviews.models.status_update.StatusUpdate` models

    
   
1584
    for custom integrations.

    
   
1585

   

    
   
1586
    Version Added:

    
   
1587
        5.0.3

    
   
1588
    """

    
   
1589

   

    
   
1590
    def create_for_integration(

    
   
1591
        self,

    
   
1592
        integration: Integration,

    
   
1593
        *,

    
   
1594
        config: IntegrationConfig,

    
   
1595
        user: User,

    
   
1596
        review_request: ReviewRequest,

    
   
1597
        change_description: Optional[ChangeDescription] = None,

    
   
1598
        service_id: Optional[str] = None,

    
   
1599
        summary: Optional[str] = None,

    
   
1600
        description: Optional[str] = None,

    
   
1601
        state: Optional[str] = None,

    
   
1602
        can_retry: bool = False,

    
   
1603
        extra_data: Dict = {},

    
   
1604
        starting_description: str = 'starting...',

    
   
1605
        waiting_description: str = 'waiting to run.',

    
   
1606
        **kwargs,

    
   
1607
    ) -> StatusUpdate:

    
   
1608
        """Return a new status update for a given integration.

    
   
1609

   

    
   
1610
        This helps with generating defaults for a status update, and putting

    
   
1611
        it in the correct initial state when running manually.

    
   
1612

   

    
   
1613
        The integration configuration will be associated with the status

    
   
1614
        update, which is important for manually running integrations when

    
   
1615
        multiple integrations are present on a review request.

    
   
1616

   

    
   
1617
        Args:

    
   
1618
            integration (reviewboard.integrations.base.Integration):

    
   
1619
                The integration that this status update will be associated

    
   
1620
                with.

    
   
1621

   

    
   
1622
            config (reviewboard.integrations.models.IntegrationConfig):

    
   
1623
                The configuration for the integration, used to provide

    
   
1624
                defaults and used for later manual runs.

    
   
1625

   

    
   
1626
            user (django.contrib.auth.models.User):

    
   
1627
                The user that the status update will be associated with.

    
   
1628

   

    
   
1629
            review_request (reviewboard.reviews.models.review_request.

    
   
1630
                            ReviewRequest):

    
   
1631
                The review request that the status update will be associated

    
   
1632
                with.

    
   
1633

   

    
   
1634
            change_description (reviewboard.changedescs.models.

    
   
1635
                                ChangeDescription, optional):

    
   
1636
                The optional change description that the status update will

    
   
1637
                be associated with.

    
   
1638

   

    
   
1639
            service_id (str, optional):

    
   
1640
                An explicit service ID for the status update.

    
   
1641

   

    
   
1642
                If not provided (or if ``None``), a slugified version of

    
   
1643
                the integration's name will be used.

    
   
1644

   

    
   
1645
            summary (str, optional):

    
   
1646
                An explicit summary for the status update.

    
   
1647

   

    
   
1648
                If not provided (or if ``None``), the integration name will

    
   
1649
                be used.

    
   
1650

   

    
   
1651
            description (str, optional):

    
   
1652
                An explicit description for the status update.

    
   
1653

   

    
   
1654
                If not provided (or if ``None``), a standardized description

    
   
1655
                will be used depending on whether the status update will be

    
   
1656
                created in manual run mode.

    
   
1657

   

    
   
1658
                See ``starting_description`` and ``waiting_description`` to

    
   
1659
                customize these strings.

    
   
1660

   

    
   
1661
            state (str, optional):

    
   
1662
                An explicit state for the status update.

    
   
1663

   

    
   
1664
                If not provided (or if ``None``), the state will be in Not

    
   
1665
                Yet Run if creating in manual run mode, or Pending otherwise.

    
   
1666

   

    
   
1667
            can_retry (bool, optional):

    
   
1668
                Whether the status update can be retried after being run.

    
   
1669

   

    
   
1670
            extra_data (dict, optional):

    
   
1671
                Extra data to store in the status update.

    
   
1672

   

    
   
1673
            starting_description (str, optional):

    
   
1674
                The description to use if creating a status update that is

    
   
1675
                immediately starting.

    
   
1676

   

    
   
1677
            waiting_description (str, optional):

    
   
1678
                The description to use if creating a status update that is

    
   
1679
                in manual run mode.

    
   
1680

   

    
   
1681
            **kwargs (dict, optional):

    
   
1682
                Additional keyword arguments for the model.

    
   
1683

   

    
   
1684
        Returns:

    
   
1685
            reviewboard.reviews.status_update.StatusUpdate:

    
   
1686
            The new status update.

    
   
1687
        """

    
   
1688
        run_manually = config.get('run_manually')

    
   
1689

   

    
   
1690
        if description is None:

    
   
1691
            if run_manually:

    
   
1692
                description = waiting_description

    
   
1693
            else:

    
   
1694
                description = starting_description

    
   
1695

   

    
   
1696
        if state is None:

    
   
1697
            if run_manually:

    
   
1698
                state = self.model.NOT_YET_RUN

    
   
1699
            else:

    
   
1700
                state = self.model.PENDING

    
   
1701

   

    
   
1702
        if not service_id:

    
   
1703
            assert integration.name

    
   
1704
            service_id = slugify(integration.name)

    
   
1705

   

    
   
1706
        status_update: StatusUpdate = self.model(

    
   
1707
            user=user,

    
   
1708
            review_request=review_request,

    
   
1709
            change_description=change_description,

    
   
1710
            service_id=service_id,

    
   
1711
            summary=summary or integration.name,

    
   
1712
            description=description,

    
   
1713
            state=state,

    
   
1714
            extra_data=dict(extra_data, **{

    
   
1715
                'can_retry': can_retry,

    
   
1716
            }),

    
   
1717
            **kwargs)

    
   
1718
        status_update.integration_config = config

    
   
1719
        status_update.save()

    
   
1720

   

    
   
1721
        return status_update
reviewboard/reviews/signals.py
reviewboard/reviews/models/status_update.py
reviewboard/reviews/tests/test_status_update.py
reviewboard/reviews/tests/test_status_update_manager.py
Loading...