|
@@ -6,9 +6,8 @@ from typing import NamedTuple
|
|
|
import click
|
|
|
from django.core.serializers import serialize
|
|
|
from django.core.serializers.json import DjangoJSONEncoder
|
|
|
-from django.db.models.fields.related import ManyToManyField
|
|
|
|
|
|
-from sentry.backup.helpers import EXCLUDED_APPS
|
|
|
+from sentry.backup.dependencies import sort_dependencies
|
|
|
|
|
|
UTC_0 = timezone(timedelta(hours=0))
|
|
|
|
|
@@ -24,103 +23,6 @@ class DatetimeSafeDjangoJSONEncoder(DjangoJSONEncoder):
|
|
|
return super().default(obj)
|
|
|
|
|
|
|
|
|
-def sort_dependencies():
|
|
|
- """
|
|
|
- Similar to Django's except that we discard the important of natural keys
|
|
|
- when sorting dependencies (i.e. it works without them).
|
|
|
- """
|
|
|
- from django.apps import apps
|
|
|
-
|
|
|
- from sentry.models.actor import Actor
|
|
|
- from sentry.models.team import Team
|
|
|
- from sentry.models.user import User
|
|
|
-
|
|
|
- # Process the list of models, and get the list of dependencies
|
|
|
- model_dependencies = []
|
|
|
- models = set()
|
|
|
- for app_config in apps.get_app_configs():
|
|
|
- if app_config.label in EXCLUDED_APPS:
|
|
|
- continue
|
|
|
-
|
|
|
- model_iterator = app_config.get_models()
|
|
|
-
|
|
|
- for model in model_iterator:
|
|
|
- models.add(model)
|
|
|
- # Add any explicitly defined dependencies
|
|
|
- if hasattr(model, "natural_key"):
|
|
|
- deps = getattr(model.natural_key, "dependencies", [])
|
|
|
- if deps:
|
|
|
- deps = [apps.get_model(*d.split(".")) for d in deps]
|
|
|
- else:
|
|
|
- deps = []
|
|
|
-
|
|
|
- # Now add a dependency for any FK relation with a model that
|
|
|
- # defines a natural key
|
|
|
- for field in model._meta.fields:
|
|
|
- rel_model = getattr(field.remote_field, "model", None)
|
|
|
- if rel_model is not None and rel_model != model:
|
|
|
- # TODO(hybrid-cloud): actor refactor.
|
|
|
- # Add cludgy conditional preventing walking actor.team_id, actor.user_id
|
|
|
- # Which avoids circular imports
|
|
|
- if model == Actor and (rel_model == Team or rel_model == User):
|
|
|
- continue
|
|
|
-
|
|
|
- deps.append(rel_model)
|
|
|
-
|
|
|
- # Also add a dependency for any simple M2M relation with a model
|
|
|
- # that defines a natural key. M2M relations with explicit through
|
|
|
- # models don't count as dependencies.
|
|
|
- many_to_many_fields = [
|
|
|
- field for field in model._meta.get_fields() if isinstance(field, ManyToManyField)
|
|
|
- ]
|
|
|
- for field in many_to_many_fields:
|
|
|
- rel_model = getattr(field.remote_field, "model", None)
|
|
|
- if rel_model is not None and rel_model != model:
|
|
|
- deps.append(rel_model)
|
|
|
-
|
|
|
- model_dependencies.append((model, deps))
|
|
|
-
|
|
|
- model_dependencies.reverse()
|
|
|
- # Now sort the models to ensure that dependencies are met. This
|
|
|
- # is done by repeatedly iterating over the input list of models.
|
|
|
- # If all the dependencies of a given model are in the final list,
|
|
|
- # that model is promoted to the end of the final list. This process
|
|
|
- # continues until the input list is empty, or we do a full iteration
|
|
|
- # over the input models without promoting a model to the final list.
|
|
|
- # If we do a full iteration without a promotion, that means there are
|
|
|
- # circular dependencies in the list.
|
|
|
- model_list = []
|
|
|
- while model_dependencies:
|
|
|
- skipped = []
|
|
|
- changed = False
|
|
|
- while model_dependencies:
|
|
|
- model, deps = model_dependencies.pop()
|
|
|
-
|
|
|
- # If all of the models in the dependency list are either already
|
|
|
- # on the final model list, or not on the original serialization list,
|
|
|
- # then we've found another model with all it's dependencies satisfied.
|
|
|
- found = True
|
|
|
- for candidate in ((d not in models or d in model_list) for d in deps):
|
|
|
- if not candidate:
|
|
|
- found = False
|
|
|
- if found:
|
|
|
- model_list.append(model)
|
|
|
- changed = True
|
|
|
- else:
|
|
|
- skipped.append((model, deps))
|
|
|
- if not changed:
|
|
|
- raise RuntimeError(
|
|
|
- "Can't resolve dependencies for %s in serialized app list."
|
|
|
- % ", ".join(
|
|
|
- f"{model._meta.app_label}.{model._meta.object_name}"
|
|
|
- for model, deps in sorted(skipped, key=lambda obj: obj[0].__name__)
|
|
|
- )
|
|
|
- )
|
|
|
- model_dependencies = skipped
|
|
|
-
|
|
|
- return model_list
|
|
|
-
|
|
|
-
|
|
|
class OldExportConfig(NamedTuple):
|
|
|
"""While we are migrating to the new backup system, we need to take care not to break the old
|
|
|
and relatively untested workflows. This model allows us to stub in the old configs."""
|