provisioning/tools/workspace-migrate.nu
2025-10-07 11:12:02 +01:00

391 lines
13 KiB
Plaintext
Executable File

#!/usr/bin/env nu
# Workspace Migration Tool
# Migrates existing workspaces to new KCL package and module loader structure
use ../core/nulib/lib_provisioning/config/accessor.nu *
# Main migration command
def main [workspace_path: string] {
print $"Migrating workspace: ($workspace_path)"
# Validate workspace exists
if not ($workspace_path | path exists) {
error make { msg: $"Workspace not found: ($workspace_path)" }
}
# Analyze current workspace
let analysis = analyze-workspace $workspace_path
print $"📊 Workspace Analysis:"
print $" Type: ($analysis.type)"
print $" KCL files: ($analysis.kcl_files | length)"
print $" Has kcl.mod: ($analysis.has_kcl_mod)"
print $" Extension references: ($analysis.extension_refs | length)"
# Perform migration steps
let migration_result = migrate-workspace $workspace_path $analysis
print $"✅ Migration completed"
print $"📁 New structure created"
print $"📄 Configuration files updated"
print $"🔄 Import paths converted"
if not ($migration_result.warnings | is-empty) {
print ""
print "⚠️ Warnings:"
for $warning in $migration_result.warnings {
print $" - ($warning)"
}
}
print ""
print "Next steps:"
print " 1. Review updated kcl.mod file"
print " 2. Load required modules: module-loader load taskservs . [modules...]"
print " 3. Test configuration: kcl run servers.k"
print " 4. Deploy: provisioning server create --infra . --check"
}
# Analyze existing workspace structure
def analyze-workspace [workspace_path: string]: nothing -> record {
let workspace_abs = ($workspace_path | path expand)
# Find KCL files
let kcl_files = try {
glob ($workspace_abs | path join "**/*.k") | where { |path| $path | path exists }
} catch { [] }
# Check for kcl.mod
let kcl_mod_path = ($workspace_abs | path join "kcl.mod")
let has_kcl_mod = ($kcl_mod_path | path exists)
# Find extension references in KCL files
let extension_refs = ($kcl_files | each { |file|
let content = try { open $file } catch { "" }
# Look for relative imports pointing to extensions
let imports = ($content | lines | where { |line| ($line | str contains "import") and ($line | str contains "../") })
$imports | each { |import| { file: $file, import: $import } }
} | flatten)
# Detect workspace type
let workspace_type = if ($extension_refs | where { |ref| $ref.import | str contains "taskservs" } | length) > 0 {
"legacy-with-taskservs"
} else if ($kcl_files | length) > 0 {
"kcl-workspace"
} else {
"basic"
}
{
path: $workspace_abs,
type: $workspace_type,
kcl_files: $kcl_files,
has_kcl_mod: $has_kcl_mod,
extension_refs: $extension_refs,
needs_migration: ((not $has_kcl_mod) or (($extension_refs | length) > 0))
}
}
# Perform workspace migration
def migrate-workspace [workspace_path: string, analysis: record]: nothing -> record {
let workspace_abs = ($workspace_path | path expand)
let warnings = []
# Step 1: Create new directory structure
print "🔧 Creating new directory structure..."
mkdir ($workspace_abs | path join ".taskservs")
mkdir ($workspace_abs | path join ".providers")
mkdir ($workspace_abs | path join ".clusters")
mkdir ($workspace_abs | path join ".manifest")
# Ensure other standard directories exist
mkdir ($workspace_abs | path join "data")
mkdir ($workspace_abs | path join "tmp")
mkdir ($workspace_abs | path join "resources")
mkdir ($workspace_abs | path join "clusters")
# Step 2: Create or update kcl.mod
print "🔧 Creating/updating kcl.mod..."
let kcl_mod_content = if $analysis.has_kcl_mod {
# Update existing kcl.mod
let existing = (open ($workspace_abs | path join "kcl.mod"))
update-kcl-mod $existing
} else {
# Create new kcl.mod
create-new-kcl-mod ($workspace_path | path basename)
}
$kcl_mod_content | save -f ($workspace_abs | path join "kcl.mod")
# Step 3: Convert import paths in KCL files
print "🔧 Converting import paths..."
let conversion_results = ($analysis.kcl_files | each { |file|
convert-imports-in-file $file
})
# Step 4: Create empty manifest files
print "🔧 Creating manifest files..."
let empty_manifest = {
last_updated: (date now | format date "%Y-%m-%d %H:%M:%S"),
workspace: $workspace_abs
}
($empty_manifest | merge { loaded_taskservs: [] }) | to yaml | save -f ($workspace_abs | path join ".manifest" | path join "taskservs.yaml")
($empty_manifest | merge { loaded_providers: [] }) | to yaml | save -f ($workspace_abs | path join ".manifest" | path join "providers.yaml")
($empty_manifest | merge { loaded_clusters: [] }) | to yaml | save -f ($workspace_abs | path join ".manifest" | path join "clusters.yaml")
# Step 5: Create .gitignore if it doesn't exist
let gitignore_path = ($workspace_abs | path join ".gitignore")
if not ($gitignore_path | path exists) {
print "🔧 Creating .gitignore..."
let gitignore_content = "# Workspace runtime data
.manifest/
data/
tmp/
*.log
# Module directories (managed by module-loader)
.taskservs/
.providers/
.clusters/
# Generated files
taskservs.k
providers.k
clusters.k
# Secrets and sensitive data
*.age
*.enc
*.key
"
$gitignore_content | save $gitignore_path
}
# Step 6: Create migration summary
let migration_info = {
migrated_at: (date now | format date "%Y-%m-%d %H:%M:%S"),
original_type: $analysis.type,
files_converted: ($conversion_results | length),
extension_refs_converted: ($analysis.extension_refs | length)
}
$migration_info | to yaml | save ($workspace_abs | path join ".migration-info.yaml")
{
success: true,
workspace: $workspace_abs,
files_converted: ($conversion_results | length),
warnings: $warnings
}
}
# Update existing kcl.mod to add provisioning dependency
def update-kcl-mod [existing_content: string]: nothing -> string {
let lines = ($existing_content | lines)
# Check if provisioning dependency already exists
let has_provisioning_dep = ($lines | any { |line| $line | str contains "provisioning" })
if $has_provisioning_dep {
return $existing_content
}
# Find [dependencies] section or add it
let deps_line_idx = ($lines | enumerate | where { |item| $item.item | str contains "[dependencies]" } | get index | first)
if ($deps_line_idx | is-empty) {
# Add dependencies section
$existing_content + "\n\n[dependencies]\nprovisioning = { path = \"~/.kcl/packages/provisioning\", version = \"0.0.1\" }\n"
} else {
# Insert after dependencies line
let before = ($lines | first ($deps_line_idx + 1))
let after = ($lines | skip ($deps_line_idx + 1))
($before | append ["provisioning = { path = \"~/.kcl/packages/provisioning\", version = \"0.0.1\" }"] | append $after | str join "\n")
}
}
# Create new kcl.mod file
def create-new-kcl-mod [workspace_name: string]: nothing -> string {
$"[package]
name = \"($workspace_name)\"
edition = \"v0.11.3\"
version = \"0.0.1\"
[dependencies]
provisioning = { path = \"~/.kcl/packages/provisioning\", version = \"0.0.1\" }
"
}
# Convert import paths in a KCL file
def convert-imports-in-file [file_path: string]: nothing -> record {
let content = try { open $file_path } catch { return { file: $file_path, status: "error", message: "Could not read file" } }
# Define import conversion patterns
let conversions = [
# Convert relative imports to package imports
{ pattern: "import ../../../kcl/", replacement: "import provisioning." }
{ pattern: "import ../../kcl/", replacement: "import provisioning." }
{ pattern: "import ../kcl/", replacement: "import provisioning." }
# Convert extension imports (these will need to be loaded via module-loader)
{ pattern: "import ../../../extensions/taskservs/", replacement: "# import .taskservs." }
{ pattern: "import ../../extensions/taskservs/", replacement: "# import .taskservs." }
{ pattern: "import ../extensions/taskservs/", replacement: "# import .taskservs." }
{ pattern: "import ../../../extensions/providers/", replacement: "# import .providers." }
{ pattern: "import ../../extensions/providers/", replacement: "# import .providers." }
{ pattern: "import ../extensions/providers/", replacement: "# import .providers." }
{ pattern: "import ../../../extensions/clusters/", replacement: "# import .clusters." }
{ pattern: "import ../../extensions/clusters/", replacement: "# import .clusters." }
{ pattern: "import ../extensions/clusters/", replacement: "# import .clusters." }
]
# Apply conversions
let updated_content = ($conversions | reduce -f $content { |acc, conv|
$acc | str replace -a $conv.pattern $conv.replacement
})
# Check if any changes were made
if $content != $updated_content {
# Backup original file
let backup_path = ($file_path + ".bak")
$content | save $backup_path
# Save updated content
$updated_content | save -f $file_path
{
file: $file_path,
status: "converted",
backup: $backup_path,
changes: true
}
} else {
{
file: $file_path,
status: "no-changes",
changes: false
}
}
}
# Dry run migration (analyze what would be changed)
export def "main dry-run" [workspace_path: string]: nothing -> record {
print $"Analyzing workspace for migration: ($workspace_path)"
let analysis = analyze-workspace $workspace_path
print $"📊 Migration Analysis Report:"
print $" Workspace: ($analysis.path)"
print $" Type: ($analysis.type)"
print $" Needs migration: ($analysis.needs_migration)"
print ""
print $" KCL files found: ($analysis.kcl_files | length)"
for $file in $analysis.kcl_files {
print $" - ($file | str replace ($analysis.path + '/') '')"
}
print ""
print $" Extension references: ($analysis.extension_refs | length)"
for $ref in $analysis.extension_refs {
let file_rel = ($ref.file | str replace ($analysis.path + '/') '')
print $" - ($file_rel): ($ref.import | str trim)"
}
if $analysis.needs_migration {
print ""
print "🔧 Migration steps that would be performed:"
print " 1. Create .taskservs/, .providers/, .clusters/, .manifest/ directories"
print " 2. Create/update kcl.mod with provisioning dependency"
print " 3. Convert import paths from relative to package-based"
print " 4. Comment out extension imports (load via module-loader)"
print " 5. Create manifest files for module tracking"
print " 6. Create .gitignore if missing"
} else {
print ""
print "✅ Workspace appears to already be migrated or needs no migration"
}
$analysis
}
# Rollback migration (restore from backups)
export def "main rollback" [workspace_path: string]: nothing -> nothing {
print $"Rolling back migration for: ($workspace_path)"
let workspace_abs = ($workspace_path | path expand)
# Find backup files
let backup_files = try {
glob ($workspace_abs | path join "**/*.bak")
} catch { [] }
if ($backup_files | is-empty) {
print "⚠️ No backup files found. Cannot rollback."
return
}
print $"Found ($backup_files | length) backup files"
# Restore each backup
for $backup in $backup_files {
let original = ($backup | str replace ".bak" "")
print $"Restoring: ($original | str replace ($workspace_abs + '/') '')"
# Copy backup back to original
cp $backup $original
# Remove backup
rm $backup
}
# Remove migration artifacts
let migration_artifacts = [
".taskservs"
".providers"
".clusters"
".manifest"
".migration-info.yaml"
]
for $artifact in $migration_artifacts {
let artifact_path = ($workspace_abs | path join $artifact)
if ($artifact_path | path exists) {
rm -rf $artifact_path
}
}
print "✅ Migration rolled back successfully"
print " Original files restored from backups"
print " Migration artifacts removed"
}
# Show migration status
export def "main status" [workspace_path: string]: nothing -> record {
let workspace_abs = ($workspace_path | path expand)
let has_new_structure = [".taskservs", ".providers", ".clusters", ".manifest"] | all { |dir|
($workspace_abs | path join $dir) | path exists
}
let migration_info_path = ($workspace_abs | path join ".migration-info.yaml")
let migration_info = if ($migration_info_path | path exists) {
open $migration_info_path
} else {
null
}
let backup_files = try {
glob ($workspace_abs | path join "**/*.bak") | length
} catch { 0 }
{
workspace: $workspace_abs,
has_new_structure: $has_new_structure,
migration_info: $migration_info,
backup_files: $backup_files,
is_migrated: ($has_new_structure and ($migration_info != null))
}
}