Skip to content

Commit 2e8763f

Browse files
committed
fix(backend): add legacy shell data migration for runtime->shellType compatibility
Add automatic data migration to convert legacy shell data from old format (using 'runtime' field) to new format (using 'shellType' field). Background: - PR #222 renamed Shell spec field from 'runtime' to 'shellType' - Existing databases with old data cannot display shells correctly - Agent startup fails because shell data cannot be properly parsed Solution: - Add migrate_legacy_shell_data() function in yaml_init.py - Migration runs automatically on application startup - Migrates both public_shells and user shells in kinds table - Migration is idempotent - only updates records using old format - Uses flag_modified() to ensure JSON field changes are detected The migration: 1. Finds all shells with 'runtime' field but no 'shellType' 2. Copies runtime value to shellType 3. Removes old runtime field for clean data 4. Commits changes with proper error handling
1 parent 04849fd commit 2e8763f

File tree

1 file changed

+126
-0
lines changed

1 file changed

+126
-0
lines changed

backend/app/core/yaml_init.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66
YAML initialization module for loading initial data from YAML files.
77
This module scans a directory for YAML files and creates initial resources.
8+
It also handles data migrations for legacy data compatibility.
89
"""
910

1011
import logging
@@ -14,8 +15,10 @@
1415

1516
import yaml
1617
from sqlalchemy.orm import Session
18+
from sqlalchemy.orm.attributes import flag_modified
1719

1820
from app.core.config import settings
21+
from app.models.kind import Kind
1922
from app.models.public_shell import PublicShell
2023
from app.models.user import User
2124
from 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+
394515
def 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

Comments
 (0)