Skip to content

Commit 1285876

Browse files
authored
🔧 chore(integrations): create scripts to save and load integration state (#92128)
1 parent a1c8bf7 commit 1285876

File tree

2 files changed

+213
-0
lines changed

2 files changed

+213
-0
lines changed

bin/load-integration-data

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#!/usr/bin/env python
2+
from sentry.runner import configure
3+
4+
configure()
5+
6+
import argparse
7+
8+
import click
9+
from django.core import serializers
10+
from django.db import IntegrityError, router
11+
12+
from sentry.integrations.models.integration import Integration
13+
from sentry.integrations.models.organization_integration import OrganizationIntegration
14+
from sentry.users.models.identity import Identity, IdentityProvider
15+
from sentry.utils.db import atomic_transaction
16+
17+
# The order in which models should be loaded to respect foreign key dependencies.
18+
MODEL_LOAD_ORDER = [
19+
"sentry.identityprovider",
20+
"sentry.integration",
21+
"sentry.identity", # Depends on sentry.identityprovider
22+
"sentry.organizationintegration", # Depends on sentry.integration (and sentry.organization)
23+
]
24+
25+
26+
def load_data(input_file, org_id):
27+
"""
28+
Loads data from a JSON file and saves it to the database.
29+
Assumes that PKs from the file should be preserved.
30+
"""
31+
click.echo(f"Reading serialized data from {input_file}...")
32+
with open(input_file) as f:
33+
serialized_data = f.read()
34+
35+
if not serialized_data.strip() or serialized_data.strip() == "[]":
36+
click.echo("Input file is empty or contains no data. Nothing to load.")
37+
return
38+
39+
click.echo("Deserializing objects...")
40+
try:
41+
deserialized_objects = list(serializers.deserialize("json", serialized_data))
42+
except Exception as e:
43+
click.echo(f"Error during deserialization: {e}")
44+
click.echo(
45+
"Please ensure the input file is a valid JSON dump created by the save_integration_data.py script."
46+
)
47+
return
48+
49+
if not deserialized_objects:
50+
click.echo("No objects were deserialized from the file.")
51+
return
52+
53+
click.echo(f"Deserialized {len(deserialized_objects)} objects.")
54+
55+
# Sort deserialized objects based on MODEL_LOAD_ORDER to handle dependencies.
56+
# Objects not in MODEL_LOAD_ORDER will be placed at the end.
57+
def get_sort_key(d_obj):
58+
model_key = f"{d_obj.object._meta.app_label}.{d_obj.object._meta.model_name}"
59+
try:
60+
return MODEL_LOAD_ORDER.index(model_key)
61+
except ValueError:
62+
return len(MODEL_LOAD_ORDER) # Put unknown models at the end
63+
64+
sorted_deserialized_objects = sorted(deserialized_objects, key=get_sort_key)
65+
66+
saved_count = 0
67+
skipped_count = 0
68+
error_count = 0
69+
70+
parsed_org_id = None
71+
if org_id:
72+
try:
73+
parsed_org_id = int(org_id)
74+
click.echo(
75+
f"Will update OrganizationIntegration objects to organization_id: {parsed_org_id}"
76+
)
77+
except ValueError:
78+
click.echo(
79+
f"Warning: Invalid org_id '{org_id}'. It will be ignored. Please provide a valid integer."
80+
)
81+
parsed_org_id = None
82+
83+
click.echo("Attempting to save objects to the database...")
84+
with atomic_transaction(
85+
using=(
86+
router.db_for_write(Integration),
87+
router.db_for_write(OrganizationIntegration),
88+
router.db_for_write(Identity),
89+
router.db_for_write(IdentityProvider),
90+
)
91+
):
92+
for deserialized_object in sorted_deserialized_objects:
93+
model_name = deserialized_object.object._meta.object_name
94+
pk = deserialized_object.object.pk
95+
96+
# If org_id is provided, update OrganizationIntegration's organization_id
97+
if parsed_org_id is not None and isinstance(
98+
deserialized_object.object, OrganizationIntegration
99+
):
100+
click.echo(
101+
f" Updating organization_id for {model_name} (PK: {pk}) to {parsed_org_id}"
102+
)
103+
deserialized_object.object.organization_id = parsed_org_id
104+
105+
try:
106+
# The deserialized_object.save() method handles saving the object
107+
# and its many-to-many data (if any). It attempts to use the PK
108+
# from the serialized data.
109+
deserialized_object.save()
110+
saved_count += 1
111+
click.echo(f" Saved: {model_name} (PK: {pk})")
112+
except IntegrityError as e:
113+
# This can occur due to PK conflict, unique constraint violation,
114+
# or a non-existent foreign key (e.g., if a referenced User or Organization
115+
# doesn't exist in the target DB).
116+
skipped_count += 1
117+
click.echo(f" Skipped: {model_name} (PK: {pk}) due to IntegrityError: {e}")
118+
except Exception as e:
119+
# Catch other potential errors during save.
120+
error_count += 1
121+
click.echo(f" Error saving: {model_name} (PK: {pk}): {e}")
122+
# Depending on severity, you might want to re-raise to stop the transaction.
123+
# For now, we'll log and continue.
124+
125+
click.echo("\nLoad process completed.")
126+
click.echo(f" Successfully saved: {saved_count} objects.")
127+
click.echo(f" Skipped (IntegrityError): {skipped_count} objects.")
128+
click.echo(f" Errors (Other): {error_count} objects.")
129+
if skipped_count > 0 or error_count > 0:
130+
click.echo(
131+
"Please check skipped/error messages. This might indicate that the target database was not clean,"
132+
)
133+
click.echo("or that required related objects (like Organizations or Users) were missing.")
134+
135+
136+
if __name__ == "__main__":
137+
parser = argparse.ArgumentParser(
138+
description="Load Sentry integration-related models from a JSON file into the database."
139+
)
140+
parser.add_argument(
141+
"--input-file",
142+
required=True,
143+
help="Path to the input JSON file containing the data to load.",
144+
)
145+
parser.add_argument(
146+
"--org-id",
147+
required=False,
148+
help="The organization ID to save integration data for.",
149+
)
150+
args = parser.parse_args()
151+
152+
load_data(args.input_file, args.org_id)

bin/save-integration-data

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/usr/bin/env python
2+
from sentry.runner import configure
3+
4+
configure()
5+
6+
import argparse
7+
8+
import click
9+
from django.core import serializers
10+
11+
from sentry.integrations.models import Integration, OrganizationIntegration
12+
from sentry.users.models.identity import Identity, IdentityProvider
13+
14+
MODELS_TO_SERIALIZE = [
15+
IdentityProvider, # Has no FKs to other serialized models
16+
Integration, # Has no FKs to other serialized models
17+
Identity, # Depends on IdentityProvider
18+
OrganizationIntegration, # Depends on Integration
19+
]
20+
21+
22+
def save_data(output_file):
23+
"""
24+
Collects data from specified models and serializes it to a JSON file.
25+
"""
26+
all_objects_to_serialize = []
27+
click.echo("Collecting data from models...")
28+
for model_cls in MODELS_TO_SERIALIZE:
29+
model_name = f"{model_cls._meta.app_label}.{model_cls._meta.model_name}"
30+
click.echo(f" Fetching from {model_name}...")
31+
# Order by PK for consistent output, though serializer might reorder.
32+
# Convert queryset to list to avoid issues with extending during iteration if any.
33+
objects = list(model_cls.objects.order_by("pk").all())
34+
all_objects_to_serialize.extend(objects)
35+
click.echo(f" Found {len(objects)} objects.")
36+
37+
if not all_objects_to_serialize:
38+
click.echo("No objects found to serialize.")
39+
serialized_data = "[]"
40+
else:
41+
click.echo(f"\nSerializing {len(all_objects_to_serialize)} objects in total...")
42+
serialized_data = serializers.serialize("json", all_objects_to_serialize, indent=2)
43+
44+
click.echo(f"Writing serialized data to {output_file}...")
45+
with open(output_file, "w") as f:
46+
f.write(serialized_data)
47+
click.echo(f"Successfully saved data to {output_file}")
48+
49+
50+
if __name__ == "__main__":
51+
parser = argparse.ArgumentParser(
52+
description="Save Sentry integration-related models to a JSON file."
53+
)
54+
parser.add_argument(
55+
"--output-file",
56+
required=True,
57+
help="Path to the output JSON file where data will be saved.",
58+
)
59+
args = parser.parse_args()
60+
61+
save_data(args.output_file)

0 commit comments

Comments
 (0)