-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
feat(uptime): Add billing seat management for detector validators #102989
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,34 +1,26 @@ | ||
| from sentry.deletions.base import ModelDeletionTask | ||
| from sentry.uptime.models import UptimeSubscription | ||
| from sentry.uptime.models import UptimeSubscription, get_detector | ||
| from sentry.uptime.subscriptions.subscriptions import delete_uptime_subscription, remove_uptime_seat | ||
|
|
||
|
|
||
| class UptimeSubscriptionDeletionTask(ModelDeletionTask[UptimeSubscription]): | ||
| def delete_instance(self, instance: UptimeSubscription) -> None: | ||
| from sentry import quotas | ||
| from sentry.constants import DataCategory | ||
| from sentry.uptime.models import get_detector | ||
| from sentry.uptime.subscriptions.subscriptions import delete_uptime_subscription | ||
|
|
||
| detector = get_detector(instance) | ||
|
|
||
| # XXX: Typically quota updates would be handled by the | ||
| # delete_uptime_detector function exposed in the | ||
| # uptime.subscriptions.subscriptions module. However if a Detector is | ||
| # deleted without using this, we need to still ensure the billing east | ||
| # is revoked. | ||
| # is revoked. This should never happen. | ||
| # | ||
| # Since the delete_uptime_detector function is already scheduling the | ||
| # detector for deletion, you may think we could remove the quota | ||
| # remove_seat call there, since it will happen here. But this would | ||
| # mean the customers quota is not freed up _immediately_ when the | ||
| # detector is deleted using that method. | ||
| remove_uptime_seat(detector) | ||
|
|
||
| # TODO(epurkhiser): It's very likely the new Workflow Engine detector | ||
| # APIs will NOT free up customers seats immediately. We'll probably | ||
| # need some other hook for this | ||
|
|
||
| # Ensure the billing seat for the parent detector is removed | ||
| quotas.backend.remove_seat(DataCategory.UPTIME, detector) | ||
|
|
||
| # Ensure the remote subscription is also removed | ||
| # Ensure the remote subscription is removed if it wasn't already (again | ||
| # it should have been as part of delete_uptime_detector) | ||
| delete_uptime_subscription(instance) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,11 +7,11 @@ | |
| from rest_framework import serializers | ||
| from rest_framework.fields import URLField | ||
|
|
||
| from sentry import audit_log | ||
| from sentry import audit_log, quotas | ||
| from sentry.api.fields import ActorField | ||
| from sentry.api.serializers.rest_framework import CamelSnakeSerializer | ||
| from sentry.auth.superuser import is_active_superuser | ||
| from sentry.constants import ObjectStatus | ||
| from sentry.constants import DataCategory, ObjectStatus | ||
| from sentry.models.environment import Environment | ||
| from sentry.uptime.models import ( | ||
| UptimeSubscription, | ||
|
|
@@ -28,6 +28,10 @@ | |
| check_url_limits, | ||
| create_uptime_detector, | ||
| create_uptime_subscription, | ||
| delete_uptime_subscription, | ||
| disable_uptime_detector, | ||
| enable_uptime_detector, | ||
| remove_uptime_seat, | ||
| update_uptime_detector, | ||
| update_uptime_subscription, | ||
| ) | ||
|
|
@@ -462,7 +466,47 @@ def create_source(self, validated_data: dict[str, Any]) -> UptimeSubscription: | |
| class UptimeDomainCheckFailureValidator(BaseDetectorTypeValidator): | ||
| data_sources = serializers.ListField(child=UptimeMonitorDataSourceValidator(), required=False) | ||
|
|
||
| def validate_enabled(self, value: bool) -> bool: | ||
| """ | ||
| Validate that enabling a detector is allowed based on seat availability. | ||
|
|
||
| This check will ONLY be performed when a detector instance is provided via | ||
| context (i.e., during updates). For creation, seat assignment is handled | ||
| in the create() method after the detector is created. | ||
| """ | ||
| detector = self.instance | ||
|
|
||
| # Only validate on updates when trying to enable a currently disabled detector | ||
| if detector and value and not detector.enabled: | ||
| result = quotas.backend.check_assign_seat(DataCategory.UPTIME, detector) | ||
| if not result.assignable: | ||
| raise serializers.ValidationError(result.reason) | ||
|
|
||
| return value | ||
|
|
||
| def create(self, validated_data): | ||
| detector = super().create(validated_data) | ||
|
|
||
| try: | ||
| enable_uptime_detector(detector, ensure_assignment=True) | ||
| except UptimeMonitorNoSeatAvailable: | ||
| # No need to do anything if we failed to handle seat | ||
| # assignment. The monitor will be created, but not enabled | ||
| pass | ||
|
|
||
| return detector | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Disabled Features Still ActivateThe |
||
|
|
||
| def update(self, instance: Detector, validated_data: dict[str, Any]) -> Detector: | ||
| # Handle seat management when enabling/disabling | ||
| was_enabled = instance.enabled | ||
| enabled = validated_data.get("enabled", was_enabled) | ||
|
|
||
| if was_enabled != enabled: | ||
| if enabled: | ||
| enable_uptime_detector(instance) | ||
| else: | ||
| disable_uptime_detector(instance) | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Conflicting Updates Lead to Detector InstabilityThe |
||
| super().update(instance, validated_data) | ||
|
|
||
| data_source = None | ||
|
|
@@ -497,5 +541,11 @@ def update_data_source(self, instance: Detector, data_source: dict[str, Any]): | |
|
|
||
| return instance | ||
|
|
||
| def create(self, validated_data): | ||
| return super().create(validated_data) | ||
| def delete(self) -> None: | ||
| assert self.instance is not None | ||
|
|
||
| remove_uptime_seat(self.instance) | ||
| uptime_subscription = get_uptime_subscription(self.instance) | ||
| delete_uptime_subscription(uptime_subscription) | ||
|
|
||
| super().delete() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👀 yeah, this does feel a bit cleaner than having the hooks -- yay it's just one surface area now! 🎉 |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to worry about
mode != UptimeMonitorMode.AUTO_DETECTED_ONBOARDINGand creating disabled Detectors? or are those not a possible for this?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah not applicable here. These will always be created in manual mode.