2
2
import os
3
3
import random
4
4
import re
5
+ import shutil
5
6
import subprocess
6
7
import time
8
+ from datetime import datetime
7
9
from pathlib import Path
8
10
from typing import Optional
9
11
@@ -19,9 +21,30 @@ def str_to_bool(value: Optional[str]) -> bool:
19
21
class ProxyExtrasDBManager :
20
22
@staticmethod
21
23
def _get_prisma_dir () -> str :
22
- """Get the path to the migrations directory"""
23
- migrations_dir = os .path .dirname (__file__ )
24
- return migrations_dir
24
+ """
25
+ Get the path to the migrations directory
26
+
27
+ Set os.environ["LITELLM_MIGRATION_DIR"] to a custom migrations directory, to support baselining db in read-only fs.
28
+ """
29
+ custom_migrations_dir = os .getenv ("LITELLM_MIGRATION_DIR" )
30
+ pkg_migrations_dir = os .path .dirname (__file__ )
31
+ if custom_migrations_dir :
32
+ # If migrations_dir exists, copy contents
33
+ if os .path .exists (custom_migrations_dir ):
34
+ # Copy contents instead of directory itself
35
+ for item in os .listdir (pkg_migrations_dir ):
36
+ src_path = os .path .join (pkg_migrations_dir , item )
37
+ dst_path = os .path .join (custom_migrations_dir , item )
38
+ if os .path .isdir (src_path ):
39
+ shutil .copytree (src_path , dst_path , dirs_exist_ok = True )
40
+ else :
41
+ shutil .copy2 (src_path , dst_path )
42
+ else :
43
+ # If directory doesn't exist, create it and copy everything
44
+ shutil .copytree (pkg_migrations_dir , custom_migrations_dir )
45
+ return custom_migrations_dir
46
+
47
+ return pkg_migrations_dir
25
48
26
49
@staticmethod
27
50
def _create_baseline_migration (schema_path : str ) -> bool :
@@ -33,27 +56,29 @@ def _create_baseline_migration(schema_path: str) -> bool:
33
56
# Create migrations/0_init directory
34
57
init_dir .mkdir (parents = True , exist_ok = True )
35
58
36
- # Generate migration SQL file
37
- migration_file = init_dir / "migration.sql"
59
+ database_url = os .getenv ("DATABASE_URL" )
38
60
39
61
try :
40
- # Generate migration diff with increased timeout
62
+ # 1. Generate migration SQL file by comparing empty state to current db state
63
+ logger .info ("Generating baseline migration..." )
64
+ migration_file = init_dir / "migration.sql"
41
65
subprocess .run (
42
66
[
43
67
"prisma" ,
44
68
"migrate" ,
45
69
"diff" ,
46
70
"--from-empty" ,
47
- "--to-schema-datamodel " ,
48
- str ( schema_path ) ,
71
+ "--to-url " ,
72
+ database_url ,
49
73
"--script" ,
50
74
],
51
75
stdout = open (migration_file , "w" ),
52
76
check = True ,
53
77
timeout = 30 ,
54
- ) # 30 second timeout
78
+ )
55
79
56
- # Mark migration as applied with increased timeout
80
+ # 3. Mark the migration as applied since it represents current state
81
+ logger .info ("Marking baseline migration as applied..." )
57
82
subprocess .run (
58
83
[
59
84
"prisma" ,
@@ -73,8 +98,10 @@ def _create_baseline_migration(schema_path: str) -> bool:
73
98
)
74
99
return False
75
100
except subprocess .CalledProcessError as e :
76
- logger .warning (f"Error creating baseline migration: { e } " )
77
- return False
101
+ logger .warning (
102
+ f"Error creating baseline migration: { e } , { e .stderr } , { e .stdout } "
103
+ )
104
+ raise e
78
105
79
106
@staticmethod
80
107
def _get_migration_names (migrations_dir : str ) -> list :
@@ -104,8 +131,85 @@ def _resolve_specific_migration(migration_name: str):
104
131
)
105
132
106
133
@staticmethod
107
- def _resolve_all_migrations (migrations_dir : str ):
108
- """Mark all existing migrations as applied"""
134
+ def _resolve_all_migrations (migrations_dir : str , schema_path : str ):
135
+ """
136
+ 1. Compare the current database state to schema.prisma and generate a migration for the diff.
137
+ 2. Run prisma migrate deploy to apply any pending migrations.
138
+ 3. Mark all existing migrations as applied.
139
+ """
140
+ database_url = os .getenv ("DATABASE_URL" )
141
+ diff_dir = (
142
+ Path (migrations_dir )
143
+ / "migrations"
144
+ / f"{ datetime .now ().strftime ('%Y%m%d%H%M%S' )} _baseline_diff"
145
+ )
146
+ try :
147
+ diff_dir .mkdir (parents = True , exist_ok = True )
148
+ except Exception as e :
149
+ if "Permission denied" in str (e ):
150
+ logger .warning (
151
+ f"Permission denied - { e } \n unable to baseline db. Set LITELLM_MIGRATION_DIR environment variable to a writable directory to enable migrations."
152
+ )
153
+ return
154
+ raise e
155
+ diff_sql_path = diff_dir / "migration.sql"
156
+
157
+ # 1. Generate migration SQL for the diff between DB and schema
158
+ try :
159
+ logger .info ("Generating migration diff between DB and schema.prisma..." )
160
+ with open (diff_sql_path , "w" ) as f :
161
+ subprocess .run (
162
+ [
163
+ "prisma" ,
164
+ "migrate" ,
165
+ "diff" ,
166
+ "--from-url" ,
167
+ database_url ,
168
+ "--to-schema-datamodel" ,
169
+ schema_path ,
170
+ "--script" ,
171
+ ],
172
+ check = True ,
173
+ timeout = 60 ,
174
+ stdout = f ,
175
+ )
176
+ except subprocess .CalledProcessError as e :
177
+ logger .warning (f"Failed to generate migration diff: { e .stderr } " )
178
+ except subprocess .TimeoutExpired :
179
+ logger .warning ("Migration diff generation timed out." )
180
+
181
+ # check if the migration was created
182
+ if not diff_sql_path .exists ():
183
+ logger .warning ("Migration diff was not created" )
184
+ return
185
+ logger .info (f"Migration diff created at { diff_sql_path } " )
186
+
187
+ # 2. Run prisma db execute to apply the migration
188
+ try :
189
+ logger .info ("Running prisma db execute to apply the migration diff..." )
190
+ result = subprocess .run (
191
+ [
192
+ "prisma" ,
193
+ "db" ,
194
+ "execute" ,
195
+ "--file" ,
196
+ str (diff_sql_path ),
197
+ "--schema" ,
198
+ schema_path ,
199
+ ],
200
+ timeout = 60 ,
201
+ check = True ,
202
+ capture_output = True ,
203
+ text = True ,
204
+ )
205
+ logger .info (f"prisma db execute stdout: { result .stdout } " )
206
+ logger .info ("✅ Migration diff applied successfully" )
207
+ except subprocess .CalledProcessError as e :
208
+ logger .warning (f"Failed to apply migration diff: { e .stderr } " )
209
+ except subprocess .TimeoutExpired :
210
+ logger .warning ("Migration diff application timed out." )
211
+
212
+ # 3. Mark all migrations as applied
109
213
migration_names = ProxyExtrasDBManager ._get_migration_names (migrations_dir )
110
214
logger .info (f"Resolving { len (migration_names )} migrations" )
111
215
for migration_name in migration_names :
@@ -126,7 +230,7 @@ def _resolve_all_migrations(migrations_dir: str):
126
230
)
127
231
128
232
@staticmethod
129
- def setup_database (schema_path : str , use_migrate : bool = False ) -> bool :
233
+ def setup_database (use_migrate : bool = False ) -> bool :
130
234
"""
131
235
Set up the database using either prisma migrate or prisma db push
132
236
Uses migrations from litellm-proxy-extras package
@@ -138,6 +242,7 @@ def setup_database(schema_path: str, use_migrate: bool = False) -> bool:
138
242
Returns:
139
243
bool: True if setup was successful, False otherwise
140
244
"""
245
+ schema_path = ProxyExtrasDBManager ._get_prisma_dir () + "/schema.prisma"
141
246
use_migrate = str_to_bool (os .getenv ("USE_PRISMA_MIGRATE" )) or use_migrate
142
247
for attempt in range (4 ):
143
248
original_dir = os .getcwd ()
@@ -200,7 +305,9 @@ def setup_database(schema_path: str, use_migrate: bool = False) -> bool:
200
305
logger .info (
201
306
"Baseline migration created, resolving all migrations"
202
307
)
203
- ProxyExtrasDBManager ._resolve_all_migrations (migrations_dir )
308
+ ProxyExtrasDBManager ._resolve_all_migrations (
309
+ migrations_dir , schema_path
310
+ )
204
311
logger .info ("✅ All migrations resolved." )
205
312
return True
206
313
elif (
0 commit comments