Browse Source

chore(domains) Rename mixed case slugs to lowercase (#45669)

Rename organization slugs that are mixed case to be all lower case. We
need to do this because mixed case slugs are incompatible with customer
domains. In order to reach 100% on saas (and prepare self-hosted) we
need to rename slugs. This change refines the previous attempt and
handles the case where the 'new' slug is already taken.

Mulligan on https://github.com/getsentry/sentry/pull/43917

This reverts commit
https://github.com/getsentry/sentry/commit/b00e76c8a95ceff0412fa129590091a2bb02329b.
Mark Story 2 years ago
parent
commit
5d1bd0bb27

+ 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: 0380_backfill_monitor_env_initial
+sentry: 0381_fix_org_slug_casing
 social_auth: 0001_initial

+ 47 - 0
src/sentry/migrations/0381_fix_org_slug_casing.py

@@ -0,0 +1,47 @@
+# Generated by Django 2.2.28 on 2023-01-31 20:37
+
+from django.db import IntegrityError, migrations
+from django.db.models.functions import Lower
+
+from sentry.constants import RESERVED_ORGANIZATION_SLUGS
+from sentry.db.models.utils import slugify_instance
+from sentry.new_migrations.migrations import CheckedMigration
+from sentry.utils.query import RangeQuerySetWrapperWithProgressBar
+
+
+def fix_org_slug_casing(apps, schema_editor):
+    Organization = apps.get_model("sentry", "Organization")
+    query = Organization.objects.exclude(slug=Lower("slug"))
+    for org in RangeQuerySetWrapperWithProgressBar(query):
+        try:
+            org.slug = org.slug.lower()
+            org.save(update_fields=["slug"])
+        except IntegrityError:
+            slugify_instance(org, org.slug, reserved=RESERVED_ORGANIZATION_SLUGS)
+            org.save(update_fields=["slug"])
+
+
+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 = False
+
+    dependencies = [
+        ("sentry", "0380_backfill_monitor_env_initial"),
+    ]
+
+    operations = [
+        migrations.RunPython(
+            fix_org_slug_casing,
+            reverse_code=migrations.RunPython.noop,
+            hints={"tables": ["sentry_organization"]},
+        )
+    ]

+ 33 - 0
tests/sentry/migrations/test_0381_fix_org_slug_casing.py

@@ -0,0 +1,33 @@
+from sentry.models.organization import Organization
+from sentry.testutils.cases import TestMigrations
+
+
+class TestOrgSlugMigration(TestMigrations):
+    migrate_from = "0380_backfill_monitor_env_initial"
+    migrate_to = "0381_fix_org_slug_casing"
+
+    def setup_before_migration(self, apps):
+        self.ok_org = self.create_organization(slug="good-slug")
+        self.rename_org = self.create_organization(slug="badslug")
+
+        self.has_lower = self.create_organization(slug="taken")
+        self.is_dupe = self.create_organization(slug="taken-dupe")
+
+        # Organization.save() corrects our bad slugs, so
+        # we need to sneak by django and coerce bad states
+        Organization.objects.filter(id=self.rename_org.id).update(slug="bAdSluG")
+        Organization.objects.filter(id=self.is_dupe.id).update(slug="TakeN")
+
+    def test(self):
+        self.ok_org.refresh_from_db()
+        self.rename_org.refresh_from_db()
+
+        assert self.ok_org.slug == "good-slug"
+        assert self.rename_org.slug == "badslug"
+
+        self.has_lower.refresh_from_db()
+        self.is_dupe.refresh_from_db()
+
+        assert self.has_lower.slug == "taken"
+        assert self.is_dupe.slug.startswith("taken-")
+        assert self.is_dupe.slug != "taken"