provisioning-core/nulib/domain/workspace/migration.nu

345 lines
12 KiB
Text
Raw Permalink Normal View History

# Workspace Migration Framework
# Handles workspace migrations between versions with backups and rollback
use std log
use platform/user/config.nu [get-workspace-path validate-workspace-exists]
use domain/workspace/version.nu [
add-migration-record get-system-version get-workspace-metadata-path
init-workspace-metadata load-workspace-metadata
]
export def get-migration-strategies [] {
{
"unknown_to_2.0.5": {
name: "Initialize metadata"
description: "Add metadata tracking to existing workspace"
from: "unknown"
to: "2.0.5"
breaking: false
auto_migrate: true
}
"2.0.0_to_2.0.5": {
name: "Workspace switching update"
description: "Add workspace switching support and metadata"
from: "2.0.0"
to: "2.0.5"
breaking: false
auto_migrate: true
}
"2.0.5_to_3.0.0": {
name: "Major version update"
description: "Migrate to v3.0.0 with breaking changes"
from: "2.0.5"
to: "3.0.0"
breaking: true
auto_migrate: false
}
}
}
export def find-migration-path [
from_version: string
to_version: string
] {
let strategies = (get-migration-strategies)
mut path = []
if $from_version == "unknown" {
$path = ($path | append {
strategy: "unknown_to_2.0.5"
details: $strategies.unknown_to_2.0.5
})
return $path
}
let strategy_key = $"($from_version)_to_($to_version)"
if ($strategy_key in $strategies) {
$path = ($path | append {
strategy: $strategy_key
details: ($strategies | get $strategy_key)
})
return $path
}
return $path
}
export def create-workspace-backup [
workspace_path: string
backup_reason: string
] {
let workspace_name = ($workspace_path | path basename)
let timestamp = (date now | format date "%Y%m%d_%H%M%S")
let backup_name = $"($workspace_name)_backup_($timestamp)"
let backup_dir = ($workspace_path | path dirname | path join ".workspace_backups")
if not ($backup_dir | path exists) {
mkdir $backup_dir
}
let backup_path = ($backup_dir | path join $backup_name)
print $"(ansi cyan)Creating backup...(ansi reset)"
let backup_result = (do {
cp -r $workspace_path $backup_path
let backup_metadata = {
original_path: $workspace_path
backup_path: $backup_path
timestamp: $timestamp
reason: $backup_reason
created: (date now | format date "%Y-%m-%dT%H:%M:%SZ")
}
$backup_metadata | to yaml | save -f ($backup_path | path join ".backup_info.yaml")
{success: true, backup_path: $backup_path, metadata: $backup_metadata}
} | complete)
if $backup_result.exit_code == 0 {
print $"(ansi green)✓(ansi reset) Backup created: [$backup_path]"
$backup_result.stdout
} else {
print $"(ansi red)✗(ansi reset) Backup failed: ($backup_result.stderr)"
{ success: false, error: $backup_result.stderr }
}
}
export def migrate-unknown-to-2_0_5 [
workspace_path: string
workspace_name: string
] {
print $"(ansi cyan)Migrating workspace to version 2.0.5...(ansi reset)"
let result = (do {
let metadata = (init-workspace-metadata $workspace_path $workspace_name)
add-migration-record $workspace_path "unknown" "2.0.5" "metadata_initialization" true "Initial metadata creation"
{ success: true, message: "Workspace migrated to version 2.0.5", metadata: $metadata }
} | complete)
if $result.exit_code == 0 { $result.stdout } else {
{ success: false, error: $result.stderr, message: "Migration failed" }
}
}
export def migrate-2_0_0-to-2_0_5 [
workspace_path: string
workspace_name: string
] {
print $"(ansi cyan)Migrating workspace from 2.0.0 to 2.0.5...(ansi reset)"
let result = (do {
let metadata_path = (get-workspace-metadata-path $workspace_path)
if ($metadata_path | path exists) {
let metadata = (load-workspace-metadata $workspace_path)
add-migration-record $workspace_path "2.0.0" "2.0.5" "version_update" true "Updated to workspace switching support"
} else {
let metadata = (init-workspace-metadata $workspace_path $workspace_name)
add-migration-record $workspace_path "2.0.0" "2.0.5" "metadata_creation" true "Added metadata tracking"
}
{ success: true, message: "Workspace migrated to version 2.0.5" }
} | complete)
if $result.exit_code == 0 { $result.stdout } else {
{ success: false, error: $result.stderr, message: "Migration failed" }
}
}
export def execute-migration [
workspace_path: string
workspace_name: string
strategy: record
] {
print ""
print $"(ansi green_bold)Migration Strategy:(ansi reset) ($strategy.details.name)"
print $"(ansi cyan)Description:(ansi reset) ($strategy.details.description)"
print $"(ansi cyan)From:(ansi reset) ($strategy.details.from) → (ansi cyan)To:(ansi reset) ($strategy.details.to)"
print ""
match $strategy.strategy {
"unknown_to_2.0.5" => { migrate-unknown-to-2_0_5 $workspace_path $workspace_name }
"2.0.0_to_2.0.5" => { migrate-2_0_0-to-2_0_5 $workspace_path $workspace_name }
_ => { return { success: false, error: $"Unknown migration strategy: ($strategy.strategy)" } }
}
}
export def migrate-workspace [
workspace_name: string
--skip-backup (-s)
--force (-f)
--target-version: string
] {
print ""
print $"(ansi green_bold)Workspace Migration(ansi reset)"
print ""
if not (validate-workspace-exists $workspace_name) {
return { success: false, error: $"Workspace '($workspace_name)' not found in registry" }
}
let workspace_path = (get-workspace-path $workspace_name)
if not ($workspace_path | path exists) {
return { success: false, error: $"Workspace directory not found: ($workspace_path)" }
}
print $"(ansi cyan)Workspace:(ansi reset) ($workspace_name)"
print $"(ansi cyan)Path:(ansi reset) ($workspace_path)"
print ""
let metadata = (load-workspace-metadata $workspace_path)
let current_version = $metadata.version.provisioning
let system_version = (get-system-version)
let target = ($target_version | default $system_version)
print $"(ansi cyan)Current version:(ansi reset) ($current_version)"
print $"(ansi cyan)Target version:(ansi reset) ($target)"
print ""
let migration_path = (find-migration-path $current_version $target)
if ($migration_path | is-empty) {
if $current_version == $target {
return { success: true, message: "Workspace is already at target version", no_migration_needed: true }
} else {
return { success: false, error: $"No migration path found from ($current_version) to ($target)" }
}
}
let has_breaking = ($migration_path | any { |m| $m.details.breaking })
if $has_breaking {
print $"(ansi yellow_bold)⚠ Warning: This migration includes breaking changes(ansi reset)"
print ""
}
if not $force {
print $"(ansi yellow)This will migrate the workspace from ($current_version) to ($target)(ansi reset)"
if not $skip_backup {
print $"(ansi cyan)A backup will be created before migration.(ansi reset)"
}
print ""
let response = (input "Continue with migration? (y/N): ")
if $response != "y" and $response != "Y" {
print $"(ansi yellow)Migration cancelled(ansi reset)"
return { success: false, cancelled: true }
}
print ""
}
mut backup_result = {success: false}
if not $skip_backup {
$backup_result = (create-workspace-backup $workspace_path "pre_migration")
if not $backup_result.success {
print $"(ansi red)✗(ansi reset) Cannot proceed without backup"
return { success: false, error: "Backup creation failed", backup_error: $backup_result.error }
}
print ""
}
mut migration_results = []
for migration in $migration_path {
let result = (execute-migration $workspace_path $workspace_name $migration)
$migration_results = ($migration_results | append $result)
if not $result.success {
print ""
print $"(ansi red_bold)✗ Migration failed(ansi reset)"
print $"(ansi red)Error:(ansi reset) ($result.error)"
print ""
if ($backup_result != null) {
print $"(ansi cyan)Backup available at:(ansi reset) ($backup_result.backup_path)"
}
print ""
return { success: false, error: $result.error, backup: $backup_result, partial_results: $migration_results }
}
print $"(ansi green)✓(ansi reset) ($migration.details.name) completed"
}
print ""
print $"(ansi green_bold)✓ Migration completed successfully(ansi reset)"
print ""
return { success: true, message: $"Workspace migrated from ($current_version) to ($target)", backup: $backup_result, migrations: $migration_results }
}
export def list-workspace-backups [
workspace_name?: string
] {
let workspace_path = if ($workspace_name | is-not-empty) {
get-workspace-path $workspace_name
} else {
null
}
let backup_base_dir = if ($workspace_path | is-not-empty) {
$workspace_path | path dirname | path join ".workspace_backups"
} else {
return []
}
if not ($backup_base_dir | path exists) { return [] }
ls $backup_base_dir
| where ($it.type == "dir")
| each { |backup|
let info_file = ($backup.name | path join ".backup_info.yaml")
if ($info_file | path exists) {
let info = (open $info_file)
{ name: ($backup.name | path basename), path: $backup.name, created: $info.created, reason: $info.reason, size: $backup.size }
} else {
{ name: ($backup.name | path basename), path: $backup.name, created: "unknown", reason: "unknown", size: $backup.size }
}
}
}
export def restore-workspace-from-backup [
backup_path: string
--force (-f)
] {
if not ($backup_path | path exists) {
return { success: false, error: $"Backup not found: ($backup_path)" }
}
let info_file = ($backup_path | path join ".backup_info.yaml")
if not ($info_file | path exists) {
return { success: false, error: "Backup metadata not found - cannot restore safely" }
}
let info = (open $info_file)
let original_path = $info.original_path
print ""
print $"(ansi green_bold)Restore Workspace from Backup(ansi reset)"
print ""
print $"(ansi cyan)Backup:(ansi reset) ($backup_path)"
print $"(ansi cyan)Original path:(ansi reset) ($original_path)"
print $"(ansi cyan)Created:(ansi reset) ($info.created)"
print $"(ansi cyan)Reason:(ansi reset) ($info.reason)"
print ""
if not $force {
print $"(ansi yellow)⚠ This will replace the current workspace at:(ansi reset)"
print $" ($original_path)"
print ""
let response = (input "Continue with restore? (y/N): ")
if $response != "y" and $response != "Y" {
print $"(ansi yellow)Restore cancelled(ansi reset)"
return { success: false, cancelled: true }
}
print ""
}
let result = (do {
if ($original_path | path exists) { rm -rf $original_path }
cp -r $backup_path $original_path
let restored_info = ($original_path | path join ".backup_info.yaml")
if ($restored_info | path exists) { rm $restored_info }
{ success: true, restored_path: $original_path }
} | complete)
if $result.exit_code == 0 {
print $"(ansi green)✓(ansi reset) Workspace restored from backup"
print ""
$result.stdout
} else {
print $"(ansi red)✗(ansi reset) Restore failed: ($result.stderr)"
{ success: false, error: $result.stderr }
}
}