Skip to content

Commit 0e28307

Browse files
fix(integrations): Add RPC method to start grace period for given organization + provider (#96613)
1 parent 394b1f8 commit 0e28307

File tree

3 files changed

+314
-2
lines changed

3 files changed

+314
-2
lines changed

src/sentry/integrations/services/integration/impl.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import logging
4+
from collections import defaultdict
45
from collections.abc import Iterable
56
from typing import TYPE_CHECKING, Any
67

@@ -12,7 +13,7 @@
1213
AlertRuleUiComponentWebhookSentEvent,
1314
)
1415
from sentry.api.paginator import OffsetPaginator
15-
from sentry.constants import SentryAppInstallationStatus
16+
from sentry.constants import ObjectStatus, SentryAppInstallationStatus
1617
from sentry.hybridcloud.rpc.pagination import RpcPaginationArgs, RpcPaginationResult
1718
from sentry.incidents.models.incident import INCIDENT_STATUS, IncidentStatus
1819
from sentry.integrations.messaging.metrics import (
@@ -358,6 +359,60 @@ def update_organization_integration(
358359
)
359360
return ois[0] if len(ois) > 0 else None
360361

362+
def start_grace_period_for_provider(
363+
self,
364+
*,
365+
organization_id: int,
366+
provider: str,
367+
grace_period_end: datetime,
368+
status: int | None = ObjectStatus.ACTIVE,
369+
skip_oldest: bool = False,
370+
) -> list[RpcOrganizationIntegration]:
371+
filter_kwargs = {
372+
"organization_id": organization_id,
373+
"integration__provider": provider,
374+
}
375+
all_ois_filter_kwargs: dict[str, Any] = {
376+
"integration__provider": provider,
377+
}
378+
379+
if status is not None:
380+
filter_kwargs["status"] = status
381+
all_ois_filter_kwargs["status"] = status
382+
383+
current_org_ois = OrganizationIntegration.objects.filter(**filter_kwargs)
384+
ois_to_update = list(current_org_ois.values_list("id", flat=True))
385+
386+
if skip_oldest:
387+
all_ois_filter_kwargs["integration__in"] = current_org_ois.values_list(
388+
"integration_id", flat=True
389+
)
390+
391+
# Get all associated OrganizationIntegrations for the Integrations used by this org
392+
all_ois = (
393+
OrganizationIntegration.objects.filter(**all_ois_filter_kwargs)
394+
.order_by("id")
395+
.distinct()
396+
)
397+
398+
# Create mapping of integration_id to list of OrganizationIntegrations
399+
integration_to_ois: dict[int, list[OrganizationIntegration]] = defaultdict(list)
400+
for oi in all_ois:
401+
integration_to_ois[oi.integration_id].append(oi)
402+
403+
for integration, ois in integration_to_ois.items():
404+
# Check if the oldest OrganizationIntegration for the Integration belongs to THIS organization
405+
# If not we want to start the grace period for this org's OrganizationIntegration
406+
if integration_to_ois[integration][0].organization_id == organization_id:
407+
ois_to_update.remove(integration_to_ois[integration][0].id)
408+
409+
updated_ois = self.update_organization_integrations(
410+
org_integration_ids=ois_to_update,
411+
grace_period_end=grace_period_end,
412+
)
413+
414+
return updated_ois
415+
361416
def add_organization(self, *, integration_id: int, org_ids: list[int]) -> RpcIntegration | None:
362417
try:
363418
integration = Integration.objects.get(id=integration_id)

src/sentry/integrations/services/integration/service.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from datetime import datetime
88
from typing import Any
99

10+
from sentry.constants import ObjectStatus
1011
from sentry.hybridcloud.rpc.pagination import RpcPaginationArgs, RpcPaginationResult
1112
from sentry.hybridcloud.rpc.service import RpcService, rpc_method
1213
from sentry.integrations.services.integration import RpcIntegration, RpcOrganizationIntegration
@@ -119,6 +120,31 @@ def get_organization_integration(
119120
)
120121
return ois[0] if len(ois) > 0 else None
121122

123+
@rpc_method
124+
@abstractmethod
125+
def start_grace_period_for_provider(
126+
self,
127+
*,
128+
organization_id: int,
129+
provider: str,
130+
grace_period_end: datetime,
131+
status: int | None = ObjectStatus.ACTIVE,
132+
skip_oldest: bool = False,
133+
) -> list[RpcOrganizationIntegration]:
134+
"""
135+
Start grace period for all OrganizationIntegrations of a given provider for an organization
136+
137+
Args:
138+
organization_id (int): The Organization whose OrganizationIntegrations will be grace perioded
139+
provider (str): The provider key - e.g. "github"
140+
grace_period_end (datetime): The grace period end date
141+
status (int, optional): The status of the OrganizationIntegrations. Defaults to ObjectStatus.ACTIVE. Put None to include all statuses.
142+
skip_oldest (bool, optional): Flag for if we want to skip grace period for the oldest OrganizationIntegration per Integration. Defaults to False.
143+
144+
Returns:
145+
list[RpcOrganizationIntegration]: The updated OrganizationIntegrations
146+
"""
147+
122148
@rpc_method
123149
@abstractmethod
124150
def organization_context(

tests/sentry/integrations/services/test_integration.py

Lines changed: 232 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from datetime import datetime, timezone
3+
from datetime import datetime, timedelta, timezone
44
from unittest.mock import MagicMock, patch
55

66
from sentry.constants import ObjectStatus
@@ -354,3 +354,234 @@ def test_send_incident_alert_missing_sentryapp(self, mock_record: MagicMock) ->
354354
assert_count_of_metric(
355355
mock_record=mock_record, outcome=EventLifecycleOutcome.FAILURE, outcome_count=1
356356
)
357+
358+
359+
@all_silo_test
360+
class StartGracePeriodForProviderTest(TestCase):
361+
def setUp(self) -> None:
362+
super().setUp()
363+
with assume_test_silo_mode(SiloMode.REGION):
364+
self.org1 = self.organization
365+
self.org2 = self.create_organization(name="Test Org 2")
366+
self.org3 = self.create_organization(name="Test Org 3")
367+
368+
with assume_test_silo_mode(SiloMode.CONTROL):
369+
370+
self.github_integration_1 = self.create_integration(
371+
organization=self.org2,
372+
name="GitHub Integration 1",
373+
provider="github",
374+
external_id="github:repo1",
375+
status=ObjectStatus.ACTIVE,
376+
)
377+
378+
self.github_integration_2 = self.create_integration(
379+
organization=self.org1,
380+
name="GitHub Integration 2",
381+
provider="github",
382+
external_id="github:repo2",
383+
status=ObjectStatus.ACTIVE,
384+
)
385+
386+
now = datetime.now(timezone.utc)
387+
# Add the same GitHub integrations to other orgs (multi-org scenario)
388+
# Org2 uses github_integration_1 and is older than org1's so org1 should get the grace period
389+
self.org1_github_oi_1 = self.create_organization_integration(
390+
integration=self.github_integration_1,
391+
organization_id=self.org1.id,
392+
status=ObjectStatus.ACTIVE,
393+
date_added=now, # Older than org 2
394+
)
395+
396+
# Org3 uses github_integration_2 and is newer than org1's
397+
self.org3_github_oi_2 = self.create_organization_integration(
398+
integration=self.github_integration_2,
399+
organization_id=self.org3.id,
400+
status=ObjectStatus.ACTIVE,
401+
date_added=now + timedelta(days=5), # Newer than org1
402+
)
403+
404+
# Get org2's OrganizationIntegration for github_integration_1
405+
self.org2_github_oi_1 = OrganizationIntegration.objects.get(
406+
organization_id=self.org2.id, integration_id=self.github_integration_1.id
407+
)
408+
# Get org1's OrganizationIntegration for github_integration_2
409+
self.org1_github_oi_2 = OrganizationIntegration.objects.get(
410+
organization_id=self.org1.id, integration_id=self.github_integration_2.id
411+
)
412+
413+
@freeze_time()
414+
def test_start_grace_period_for_provider_github_with_skip_oldest(self) -> None:
415+
grace_period_end = datetime.now(timezone.utc) + timedelta(days=7)
416+
with assume_test_silo_mode(SiloMode.REGION):
417+
grace_perioded_ois = integration_service.start_grace_period_for_provider(
418+
organization_id=self.org1.id,
419+
provider="github",
420+
grace_period_end=grace_period_end,
421+
status=ObjectStatus.ACTIVE,
422+
skip_oldest=True,
423+
)
424+
425+
# Expected behavior with skip_oldest=True:
426+
# - org1_github_oi_1 should get grace perioded since org 2's OI is older
427+
# - org1_github_oi_2 should not get grace perioded (since it's the oldest OI for this integration)
428+
# - org3_github_oi_2 should not get grace perioded (OIs that aren't from the downgrading org should be untouched)
429+
430+
lof_grace_perioded_ois = [oi.id for oi in grace_perioded_ois]
431+
432+
with assume_test_silo_mode(SiloMode.CONTROL):
433+
self.org1_github_oi_1.refresh_from_db()
434+
self.org1_github_oi_2.refresh_from_db()
435+
self.org3_github_oi_2.refresh_from_db()
436+
self.org2_github_oi_1.refresh_from_db()
437+
438+
# Assertions for github_integration_1
439+
assert self.org1_github_oi_1.id in lof_grace_perioded_ois
440+
assert self.org1_github_oi_1.grace_period_end == grace_period_end
441+
assert self.org2_github_oi_1.grace_period_end is None
442+
assert self.org2_github_oi_1.id not in lof_grace_perioded_ois
443+
444+
# Assertions for github_integration_2
445+
assert self.org1_github_oi_2.id not in lof_grace_perioded_ois
446+
assert self.org1_github_oi_2.grace_period_end is None
447+
assert self.org3_github_oi_2.id not in lof_grace_perioded_ois
448+
assert self.org3_github_oi_2.grace_period_end is None
449+
450+
assert len(lof_grace_perioded_ois) == 1, "Should only include org1_github_oi_1"
451+
452+
def test_start_grace_period_for_provider_github_without_skip_oldest(self) -> None:
453+
grace_period_end = datetime.now(timezone.utc) + timedelta(days=7)
454+
455+
grace_perioded_ois = integration_service.start_grace_period_for_provider(
456+
organization_id=self.org1.id,
457+
provider="github",
458+
grace_period_end=grace_period_end,
459+
status=ObjectStatus.ACTIVE,
460+
skip_oldest=False,
461+
)
462+
463+
# Expected behavior with skip_oldest=False:
464+
# - Both org1's GitHub OrganizationIntegrations should get grace perioded
465+
# - Other orgs' OIs should NOT be included
466+
467+
lof_grace_perioded_ois = [oi.id for oi in grace_perioded_ois]
468+
469+
with assume_test_silo_mode(SiloMode.CONTROL):
470+
self.org1_github_oi_1.refresh_from_db()
471+
self.org1_github_oi_2.refresh_from_db()
472+
473+
ois_grace_perioded = OrganizationIntegration.objects.filter(
474+
organization_id=self.org1.id,
475+
grace_period_end__isnull=False,
476+
)
477+
478+
for oi in ois_grace_perioded:
479+
assert oi.id in lof_grace_perioded_ois
480+
assert oi.grace_period_end == grace_period_end
481+
482+
assert self.org1_github_oi_1.id in lof_grace_perioded_ois
483+
assert self.org1_github_oi_2.id in lof_grace_perioded_ois
484+
assert self.org1_github_oi_1.grace_period_end == grace_period_end
485+
assert self.org1_github_oi_2.grace_period_end == grace_period_end
486+
487+
assert len(lof_grace_perioded_ois) == 2, "Both org1's GitHub OIs should be grace perioded"
488+
489+
def test_start_grace_period_for_provider_github_for_all_statuses(self) -> None:
490+
grace_period_end = datetime.now(timezone.utc) + timedelta(days=7)
491+
self.github_integration_3 = self.create_integration(
492+
organization=self.org1,
493+
name="GitHub Integration 3",
494+
provider="github",
495+
external_id="github:repo3",
496+
status=None,
497+
)
498+
with assume_test_silo_mode(SiloMode.CONTROL):
499+
self.org1_github_oi_3 = OrganizationIntegration.objects.get(
500+
organization_id=self.org1.id, integration_id=self.github_integration_3.id
501+
)
502+
self.org1_github_oi_3.status = ObjectStatus.HIDDEN
503+
self.org1_github_oi_3.save()
504+
505+
grace_perioded_ois = integration_service.start_grace_period_for_provider(
506+
organization_id=self.org1.id,
507+
provider="github",
508+
grace_period_end=grace_period_end,
509+
status=None,
510+
skip_oldest=False,
511+
)
512+
513+
# Expected behavior with skip_oldest=False:
514+
# - Both org1's GitHub OrganizationIntegrations should get grace perioded
515+
# - Other orgs' OIs should NOT be included
516+
517+
lof_grace_perioded_ois = [oi.id for oi in grace_perioded_ois]
518+
519+
with assume_test_silo_mode(SiloMode.CONTROL):
520+
self.org1_github_oi_1.refresh_from_db()
521+
self.org1_github_oi_2.refresh_from_db()
522+
self.org1_github_oi_3.refresh_from_db()
523+
524+
ois_grace_perioded = OrganizationIntegration.objects.filter(
525+
organization_id=self.org1.id,
526+
grace_period_end__isnull=False,
527+
)
528+
529+
for oi in ois_grace_perioded:
530+
assert oi.id in lof_grace_perioded_ois
531+
assert oi.grace_period_end == grace_period_end
532+
533+
assert len(lof_grace_perioded_ois) == 3, "All org1's GitHub OIs should be grace perioded"
534+
535+
def test_start_grace_period_for_provider_github_for_all_statuses_with_skip_oldest(self) -> None:
536+
grace_period_end = datetime.now(timezone.utc) + timedelta(days=7)
537+
self.github_integration_3 = self.create_integration(
538+
organization=self.org1,
539+
name="GitHub Integration 3",
540+
provider="github",
541+
external_id="github:repo3",
542+
status=None,
543+
)
544+
with assume_test_silo_mode(SiloMode.CONTROL):
545+
self.org1_github_oi_3 = OrganizationIntegration.objects.get(
546+
organization_id=self.org1.id, integration_id=self.github_integration_3.id
547+
)
548+
self.org1_github_oi_3.status = ObjectStatus.HIDDEN
549+
self.org1_github_oi_3.save()
550+
551+
grace_perioded_ois = integration_service.start_grace_period_for_provider(
552+
organization_id=self.org1.id,
553+
provider="github",
554+
grace_period_end=grace_period_end,
555+
status=None,
556+
skip_oldest=True,
557+
)
558+
559+
# Expected behavior with skip_oldest=False:
560+
# - Both org1's GitHub OrganizationIntegrations should get grace perioded
561+
# - Other orgs' OIs should NOT be included
562+
563+
lof_grace_perioded_ois = [oi.id for oi in grace_perioded_ois]
564+
565+
with assume_test_silo_mode(SiloMode.CONTROL):
566+
self.org1_github_oi_1.refresh_from_db()
567+
self.org1_github_oi_2.refresh_from_db()
568+
self.org1_github_oi_3.refresh_from_db()
569+
ois_grace_perioded = OrganizationIntegration.objects.filter(
570+
organization_id=self.org1.id,
571+
grace_period_end__isnull=False,
572+
)
573+
574+
for oi in ois_grace_perioded:
575+
assert oi.id in lof_grace_perioded_ois
576+
assert oi.grace_period_end == grace_period_end
577+
578+
assert (
579+
len(lof_grace_perioded_ois) == 1
580+
), "Only org1_github_oi_1 should be grace perioded since it's NOT the oldest OI for its integration"
581+
582+
assert self.org1_github_oi_1.id in lof_grace_perioded_ois
583+
assert self.org1_github_oi_1.grace_period_end == grace_period_end
584+
assert self.org1_github_oi_2.id not in lof_grace_perioded_ois
585+
assert self.org1_github_oi_2.grace_period_end is None
586+
assert self.org1_github_oi_3.id not in lof_grace_perioded_ois
587+
assert self.org1_github_oi_3.grace_period_end is None

0 commit comments

Comments
 (0)