Browse Source

feat(HC): Adds backfill for organization mappings via Org Update outbox (#50212)

Gabe Villalobos 1 year ago
parent
commit
b5a1d2ff3e

+ 1 - 1
migrations_lockfile.txt

@@ -6,5 +6,5 @@ To resolve this, rebase against latest master and regenerate your migration. Thi
 will then be regenerated, and you should be able to merge without conflicts.
 
 nodestore: 0002_nodestore_no_dictfield
-sentry: 0477_control_avatars
+sentry: 0478_backfill_organization_mappings_via_outbox
 social_auth: 0001_initial

+ 81 - 0
src/sentry/migrations/0478_backfill_organization_mappings_via_outbox.py

@@ -0,0 +1,81 @@
+# Generated by Django 2.2.28 on 2023-06-01 18:56
+from enum import IntEnum
+
+from django.db import migrations
+
+from sentry.new_migrations.migrations import CheckedMigration
+from sentry.utils.query import RangeQuerySetWrapperWithProgressBar
+
+
+class OutboxCategory(IntEnum):
+    USER_UPDATE = 0
+    WEBHOOK_PROXY = 1
+    ORGANIZATION_UPDATE = 2
+    ORGANIZATION_MEMBER_UPDATE = 3
+    VERIFY_ORGANIZATION_MAPPING = 4
+    AUDIT_LOG_EVENT = 5
+    USER_IP_EVENT = 6
+    INTEGRATION_UPDATE = 7
+    PROJECT_UPDATE = 8
+    API_APPLICATION_UPDATE = 9
+    SENTRY_APP_INSTALLATION_UPDATE = 10
+    TEAM_UPDATE = 11
+    ORGANIZATION_INTEGRATION_UPDATE = 12
+    ORGANIZATION_MEMBER_CREATE = 13
+
+
+class OutboxScope(IntEnum):
+    ORGANIZATION_SCOPE = 0
+    USER_SCOPE = 1
+    WEBHOOK_SCOPE = 2
+    AUDIT_LOG_SCOPE = 3
+    USER_IP_SCOPE = 4
+    INTEGRATION_SCOPE = 5
+    APP_SCOPE = 6
+    TEAM_SCOPE = 7
+
+
+class OrganizationStatus(IntEnum):
+    ACTIVE = 0
+    PENDING_DELETION = 1
+    DELETION_IN_PROGRESS = 2
+
+
+def backfill_org_mapping_via_outbox(apps, schema_editor):
+    Organization = apps.get_model("sentry", "Organization")
+    RegionOutbox = apps.get_model("sentry", "RegionOutbox")
+
+    for org in RangeQuerySetWrapperWithProgressBar(Organization.objects.all()):
+        if org.status != OrganizationStatus.DELETION_IN_PROGRESS:
+            RegionOutbox(
+                shard_scope=OutboxScope.ORGANIZATION_SCOPE,
+                shard_identifier=org.id,
+                category=OutboxCategory.ORGANIZATION_UPDATE,
+                object_identifier=org.id,
+            ).save()
+
+
+class Migration(CheckedMigration):
+    # This flag is used to mark that a migration shouldn't be automatically run in production. For
+    # the most part, this should only be used for operations where it's safe to run the migration
+    # after your code has deployed. So this should not be used for most operations that alter the
+    # schema of a table.
+    # Here are some things that make sense to mark as dangerous:
+    # - Large data migrations. Typically we want these to be run manually by ops so that they can
+    #   be monitored and not block the deploy for a long period of time while they run.
+    # - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to
+    #   have ops run this and not block the deploy. Note that while adding an index is a schema
+    #   change, it's completely safe to run the operation after the code has deployed.
+    is_dangerous = True
+
+    dependencies = [
+        ("sentry", "0477_control_avatars"),
+    ]
+
+    operations = [
+        migrations.RunPython(
+            backfill_org_mapping_via_outbox,
+            reverse_code=migrations.RunPython.noop,
+            hints={"tables": ["sentry_organization", "sentry_regionoutbox"]},
+        )
+    ]

+ 64 - 0
tests/sentry/migrations/test_0478_backfill_organization_mappings_via_outbox.py

@@ -0,0 +1,64 @@
+import pytest
+
+from sentry.models import OrganizationMapping, OrganizationStatus
+from sentry.testutils.cases import TestMigrations
+from sentry.testutils.outbox import outbox_runner
+
+
+class BackfillOrganizationMappingsViaOutboxTest(TestMigrations):
+    migrate_from = "0477_control_avatars"
+    migrate_to = "0478_backfill_organization_mappings_via_outbox"
+
+    def setup_initial_state(self):
+        self.org_without_mapping = self.create_organization(name="foo", slug="foo-slug")
+        self.org_with_existing_mapping = self.create_organization(name="bar", slug="bar-slug")
+        self.org_with_mismatching_mapping = self.create_organization(
+            name="foobar", slug="foobar-slug"
+        )
+
+        # Delete the org mapping for one of the organizations
+        OrganizationMapping.objects.get(organization_id=self.org_without_mapping.id).delete()
+
+        self.org_deletion_in_progress = self.create_organization(
+            name="deleteme", slug="noimportante", status=OrganizationStatus.DELETION_IN_PROGRESS
+        )
+        # Clear the org mapping for the org pending deletion
+        OrganizationMapping.objects.get(organization_id=self.org_deletion_in_progress.id).delete()
+
+        mismatch_mapping = OrganizationMapping.objects.get(
+            organization_id=self.org_with_mismatching_mapping.id
+        )
+        mismatch_mapping.name = "old_name"
+        mismatch_mapping.slug = "old-slug"
+        mismatch_mapping.save()
+
+    def test_backfill_of_org_mappings(self):
+        with outbox_runner():
+            pass
+
+        newly_created_org_mapping = OrganizationMapping.objects.get(
+            organization_id=self.org_without_mapping.id
+        )
+        assert newly_created_org_mapping.slug == self.org_without_mapping.slug
+        assert newly_created_org_mapping.name == self.org_without_mapping.name
+        assert newly_created_org_mapping.customer_id == self.org_without_mapping.customer_id
+        assert newly_created_org_mapping.status == self.org_without_mapping.status
+
+        updated_org_mapping = OrganizationMapping.objects.get(
+            organization_id=self.org_with_mismatching_mapping.id
+        )
+        assert updated_org_mapping.slug == self.org_with_mismatching_mapping.slug
+        assert updated_org_mapping.name == self.org_with_mismatching_mapping.name
+        assert updated_org_mapping.customer_id == self.org_with_mismatching_mapping.customer_id
+        assert updated_org_mapping.status == self.org_with_mismatching_mapping.status
+
+        untouched_org_mapping = OrganizationMapping.objects.get(
+            organization_id=self.org_with_existing_mapping.id
+        )
+        assert untouched_org_mapping.slug == self.org_with_existing_mapping.slug
+        assert untouched_org_mapping.name == self.org_with_existing_mapping.name
+        assert untouched_org_mapping.customer_id == self.org_with_existing_mapping.customer_id
+        assert untouched_org_mapping.status == self.org_with_existing_mapping.status
+
+        with pytest.raises(OrganizationMapping.DoesNotExist):
+            OrganizationMapping.objects.get(organization_id=self.org_deletion_in_progress.id)