55"""
66YAML initialization module for loading initial data from YAML files.
77This module scans a directory for YAML files and creates initial resources.
8+ It also handles data migrations for legacy data compatibility.
89"""
910
1011import logging
1415
1516import yaml
1617from sqlalchemy .orm import Session
18+ from sqlalchemy .orm .attributes import flag_modified
1719
1820from app .core .config import settings
21+ from app .models .kind import Kind
1922from app .models .public_shell import PublicShell
2023from app .models .user import User
2124from app .services .k_batch import batch_service
@@ -391,10 +394,129 @@ def scan_and_apply_yaml_directory(
391394 }
392395
393396
397+ def migrate_legacy_shell_data (db : Session ) -> Dict [str , Any ]:
398+ """
399+ Migrate legacy shell data from old format (using 'runtime') to new format (using 'shellType').
400+
401+ This migration is needed because PR #222 changed the Shell spec field from 'runtime' to 'shellType'.
402+ Existing data in the database may still use the old 'runtime' field name.
403+
404+ The migration is idempotent - it only updates records that still use the old format.
405+
406+ Args:
407+ db: Database session
408+
409+ Returns:
410+ Summary of migration operations
411+ """
412+ logger .info ("Starting legacy shell data migration..." )
413+
414+ migrated_public_shells = 0
415+ migrated_user_shells = 0
416+ errors = []
417+
418+ # Migrate public_shells table
419+ try :
420+ public_shells = (
421+ db .query (PublicShell )
422+ .filter (PublicShell .is_active == True ) # noqa: E712
423+ .all ()
424+ )
425+
426+ for shell in public_shells :
427+ if not isinstance (shell .json , dict ):
428+ continue
429+
430+ spec = shell .json .get ("spec" , {})
431+ if not isinstance (spec , dict ):
432+ continue
433+
434+ # Check if using old 'runtime' field instead of 'shellType'
435+ if "runtime" in spec and "shellType" not in spec :
436+ # Migrate: copy runtime to shellType
437+ shell .json ["spec" ]["shellType" ] = spec ["runtime" ]
438+ # Remove old runtime field for clean data
439+ del shell .json ["spec" ]["runtime" ]
440+ flag_modified (shell , "json" )
441+ db .add (shell )
442+ migrated_public_shells += 1
443+ logger .info (
444+ f"Migrated public shell '{ shell .name } ': "
445+ f"runtime -> shellType = { shell .json ['spec' ]['shellType' ]} "
446+ )
447+
448+ if migrated_public_shells > 0 :
449+ db .commit ()
450+ logger .info (f"Committed { migrated_public_shells } public shell migrations" )
451+
452+ except Exception as e :
453+ logger .error (f"Error migrating public shells: { e } " )
454+ errors .append (f"public_shells: { str (e )} " )
455+ db .rollback ()
456+
457+ # Migrate user shells in kinds table
458+ try :
459+ user_shells = (
460+ db .query (Kind )
461+ .filter (
462+ Kind .kind == "Shell" ,
463+ Kind .is_active == True , # noqa: E712
464+ )
465+ .all ()
466+ )
467+
468+ for shell in user_shells :
469+ if not isinstance (shell .json , dict ):
470+ continue
471+
472+ spec = shell .json .get ("spec" , {})
473+ if not isinstance (spec , dict ):
474+ continue
475+
476+ # Check if using old 'runtime' field instead of 'shellType'
477+ if "runtime" in spec and "shellType" not in spec :
478+ # Migrate: copy runtime to shellType
479+ shell .json ["spec" ]["shellType" ] = spec ["runtime" ]
480+ # Remove old runtime field for clean data
481+ del shell .json ["spec" ]["runtime" ]
482+ flag_modified (shell , "json" )
483+ db .add (shell )
484+ migrated_user_shells += 1
485+ logger .info (
486+ f"Migrated user shell '{ shell .name } ' (user_id={ shell .user_id } ): "
487+ f"runtime -> shellType = { shell .json ['spec' ]['shellType' ]} "
488+ )
489+
490+ if migrated_user_shells > 0 :
491+ db .commit ()
492+ logger .info (f"Committed { migrated_user_shells } user shell migrations" )
493+
494+ except Exception as e :
495+ logger .error (f"Error migrating user shells: { e } " )
496+ errors .append (f"user_shells: { str (e )} " )
497+ db .rollback ()
498+
499+ total_migrated = migrated_public_shells + migrated_user_shells
500+ logger .info (
501+ f"Legacy shell data migration complete: "
502+ f"{ migrated_public_shells } public shells, "
503+ f"{ migrated_user_shells } user shells migrated"
504+ )
505+
506+ return {
507+ "status" : "completed" if not errors else "completed_with_errors" ,
508+ "migrated_public_shells" : migrated_public_shells ,
509+ "migrated_user_shells" : migrated_user_shells ,
510+ "total_migrated" : total_migrated ,
511+ "errors" : errors if errors else None ,
512+ }
513+
514+
394515def run_yaml_initialization (db : Session ) -> Dict [str , Any ]:
395516 """
396517 Main entry point for YAML initialization.
397518 Scans the configured directory and applies all YAML resources.
519+ Also runs data migrations for legacy compatibility.
398520
399521 Args:
400522 db: Database session
@@ -408,6 +530,10 @@ def run_yaml_initialization(db: Session) -> Dict[str, Any]:
408530
409531 logger .info ("Starting YAML initialization..." )
410532
533+ # Run legacy data migration first
534+ migration_result = migrate_legacy_shell_data (db )
535+ logger .info (f"Legacy data migration result: { migration_result } " )
536+
411537 # Ensure default admin user exists
412538 try :
413539 logger .info ("Ensuring default admin user exists..." )
0 commit comments