2025-10-07 10:32:04 +01:00

481 lines
14 KiB
Plaintext

# KCL Packaging Library
# Functions for packaging and distributing KCL modules
# Author: JesusPerezLorenzo
# Date: 2025-09-29
use config/accessor.nu *
use utils *
# Package core provisioning KCL schemas
export def "pack-core" [
--output: string = "" # Output directory (from config if not specified)
--version: string = "" # Version override
] {
_print "📦 Packaging core provisioning schemas..."
# Get config
let dist_config = (get-distribution-config)
let kcl_config = (get-kcl-config)
# Get pack path from config or use provided output
let pack_path = if ($output | is-empty) {
$dist_config.pack_path
} else {
$output
}
# Get core module path from config
let base_path = ($env.PROVISIONING_CONFIG? | default ($env.PROVISIONING? | default ""))
if ($base_path | is-empty) {
error make {msg: "PROVISIONING_CONFIG or PROVISIONING environment variable must be set"}
}
let core_module = ($kcl_config.core_module | str replace --all "{{paths.base}}" $base_path)
let core_path = $core_module
# Get version from config or use provided
let core_version = if ($version | is-empty) {
$kcl_config.core_version
} else {
$version
}
# Ensure pack_path exists and get absolute path
mkdir $pack_path
let abs_pack_path = ($pack_path | path expand)
# Change to the KCL module directory to run packaging from inside
cd $core_path
# Check if kcl mod pkg is supported
let help_result = (^kcl mod --help | complete)
let has_pkg = ($help_result.stdout | str contains "pkg")
if not $has_pkg {
_print $" ⚠️ KCL does not support 'kcl mod pkg'"
_print $" 💡 Please upgrade to KCL 0.11.3+ for packaging support"
error make {msg: "KCL packaging not supported in this version"}
}
# Run kcl mod pkg from inside the module directory with --target
_print $" Running: kcl mod pkg --target ($abs_pack_path)"
let result = (^kcl mod pkg --target $abs_pack_path | complete)
if $result.exit_code != 0 {
error make {msg: $"Failed to package core: ($result.stderr)"}
}
_print $" ✓ KCL packaging completed"
# Find the generated package in the target directory (kcl creates .tar files)
cd $abs_pack_path
let package_files = (glob *.tar)
if ($package_files | is-empty) {
_print $" ⚠️ No .tar file created in ($abs_pack_path)"
_print $" 💡 Check if kcl.mod is properly configured"
error make {msg: "KCL packaging did not create output file"}
}
let package_file = ($package_files | first)
_print $" ✓ Package: ($package_file)"
# Generate metadata
generate-package-metadata $pack_path "provisioning_core" $core_version $package_file
return $package_file
}
# Package a provider module
export def "pack-provider" [
provider: string, # Provider name
--output: string = "" # Output directory
--version: string = "" # Version override
] {
_print $"📦 Packaging provider: ($provider)..."
# Get config
let dist_config = (get-distribution-config)
let pack_path = if ($output | is-empty) {
$dist_config.pack_path
} else {
$output
}
# Get provider path from config
let config = (get-config)
let providers_base = ($config | get paths.providers)
let provider_path = ($providers_base | path join $provider "kcl")
if not ($provider_path | path exists) {
error make {msg: $"Provider not found: ($provider) at ($provider_path)"}
}
# Ensure pack_path exists and get absolute path
mkdir $pack_path
let abs_pack_path = ($pack_path | path expand)
# Change to the provider KCL directory to run packaging from inside
cd $provider_path
# Run kcl mod pkg with target directory
_print $" Running: kcl mod pkg --target ($abs_pack_path)"
let result = (^kcl mod pkg --target $abs_pack_path | complete)
if $result.exit_code != 0 {
error make {msg: $"Failed to package provider: ($result.stderr)"}
}
_print $" ✓ Provider ($provider) packaged"
# Find the generated package in the target directory
cd $abs_pack_path
let package_files = (glob *.tar)
if ($package_files | is-empty) {
error make {msg: "No package file found"}
}
let package_file = ($package_files | first)
_print $" ✓ Package: ($package_file)"
# Read version from kcl.mod if not provided
let pkg_version = if ($version | is-empty) {
let kcl_mod = ($provider_path | path join "kcl.mod")
if ($kcl_mod | path exists) {
parse-kcl-version $kcl_mod
} else {
"0.0.1"
}
} else {
$version
}
# Generate metadata
generate-package-metadata $pack_path $"($provider)_prov" $pkg_version $package_file
return $package_file
}
# Package all discovered providers
export def "pack-all-providers" [
--output: string = "" # Output directory
] {
use kcl_module_loader.nu *
let dist_config = (get-distribution-config)
let pack_path = if ($output | is-empty) {
$dist_config.pack_path
} else {
$output
}
_print "📦 Packaging all providers..."
let providers = (discover-kcl-modules "providers")
mut packaged = []
for provider in $providers {
# Use error handling without complete since pack-provider is internal
let pack_result = (do -i {
pack-provider $provider.name --output $pack_path
})
if ($pack_result | is-not-empty) and not ($pack_result | describe | str contains "error") {
$packaged = ($packaged | append {name: $provider.name, path: $pack_result, status: "success"})
} else {
_print $" ❌ Failed to package ($provider.name)"
$packaged = ($packaged | append {name: $provider.name, path: "", status: "failed"})
}
}
_print ""
_print $"✅ Packaged ($packaged | where status == success | length)/($packaged | length) providers"
return $packaged
}
# Generate package metadata
def "generate-package-metadata" [
pack_path: string,
module_name: string,
version: string,
package_file: string
] {
let dist_config = (get-distribution-config)
let registry_path = $dist_config.registry_path
mkdir $registry_path
let metadata_file = ($registry_path | path join $"($module_name).json")
let dist_metadata = ($dist_config | get metadata)
let metadata = {
name: $module_name
version: $version
package_file: $package_file
created: (date now | format date "%Y-%m-%d %H:%M:%S")
maintainer: $dist_metadata.maintainer
repository: $dist_metadata.repository
license: $dist_metadata.license
homepage: $dist_metadata.homepage
}
$metadata | to json | save -f $metadata_file
_print $" ✓ Metadata: ($metadata_file)"
}
# Parse version from kcl.mod
def "parse-kcl-version" [
kcl_mod_path: string
]: nothing -> string {
let content = (open $kcl_mod_path)
let lines = ($content | lines)
for line in $lines {
if ($line | str starts-with "version") {
let version = ($line | parse 'version = "{version}"' | get version.0? | default "0.0.1")
return $version
}
}
return "0.0.1"
}
# List packaged modules
export def "list-packages" [
--format: string = "table" # Output format
] {
let dist_config = (get-distribution-config)
let pack_path = $dist_config.pack_path
if not ($pack_path | path exists) {
_print "No packages found"
return []
}
# Look for .tar files
cd $pack_path
let tar_files = (glob *.tar)
let packages = $tar_files | each {|file|
let stats = (ls $file | first)
{
name: ($file | path basename | str replace ".tar" "")
path: $file
size: $stats.size
modified: $stats.modified
}
}
match $format {
"json" => ($packages | to json)
"yaml" => ($packages | to yaml)
_ => ($packages | table)
}
}
# Clean old packages
export def "clean-packages" [
--keep-latest: int = 3 # Number of latest versions to keep
--dry-run # Show what would be deleted
] {
let dist_config = (get-distribution-config)
let pack_path = $dist_config.pack_path
if not ($pack_path | path exists) {
_print "No packages directory found"
return
}
_print $"🗑️ Cleaning packages \(keeping latest ($keep_latest)\)..."
cd $pack_path
let tar_files = (glob *.tar)
if ($tar_files | is-empty) {
_print " No packages to clean"
return
}
let packages = ($tar_files | each {|file|
let stats = (ls $file | first)
{
name: $file
modified: $stats.modified
}
} | sort-by modified --reverse)
let to_delete = ($packages | skip $keep_latest)
if ($to_delete | is-empty) {
_print " Nothing to clean"
return
}
# Get registry path for metadata cleanup
let dist_config = (get-distribution-config)
let registry_path = $dist_config.registry_path
for pkg in $to_delete {
let pkg_basename = ($pkg.name | path basename)
# Extract package name without version (e.g., "aws_prov_0.0.1.tar" -> "aws_prov")
let pkg_name = ($pkg_basename | str replace -r '_\d+\.\d+\.\d+\.tar$' '')
let metadata_file = ($registry_path | path join $"($pkg_name).json")
if $dry_run {
_print $" [DRY RUN] Would delete package: ($pkg_basename)"
if ($metadata_file | path exists) {
_print $" [DRY RUN] Would delete metadata: ($metadata_file | path basename)"
}
} else {
rm $pkg.name
_print $" ✓ Deleted package: ($pkg_basename)"
# Also delete corresponding metadata if it exists
if ($metadata_file | path exists) {
rm $metadata_file
_print $" ✓ Deleted metadata: ($metadata_file | path basename)"
}
}
}
let deleted_count = ($to_delete | length)
_print $"✅ Cleaned ($deleted_count) old packages"
}
# Remove specific package and its metadata
export def "remove-package" [
package_name: string, # Package name (e.g., "aws_prov", "upcloud_prov", "provisioning_core")
--force = false # Skip confirmation prompt
] {
_print $"🗑️ Removing package: ($package_name)"
let dist_config = (get-distribution-config)
let pack_path = $dist_config.pack_path
let registry_path = $dist_config.registry_path
# Find all versions of this package
cd $pack_path
let package_pattern = $"($package_name)*.tar"
let matching_packages = (glob $package_pattern)
if ($matching_packages | is-empty) {
_print $" ❌ No packages found matching: ($package_name)"
return
}
# Show what will be deleted
_print ""
_print " Packages to remove:"
for pkg in $matching_packages {
_print $" • ($pkg | path basename)"
}
# Check for metadata
let metadata_file = ($registry_path | path join $"($package_name).json")
if ($metadata_file | path exists) {
_print $" • ($metadata_file | path basename) (metadata)"
}
# Confirmation unless forced
if not $force {
_print ""
let response = (input $"⚠️ Remove ($matching_packages | length) package\(s\) and metadata? \(y/N\): ")
if ($response | str downcase) != "y" {
_print "❌ Cancelled"
return
}
}
# Delete packages
_print ""
for pkg in $matching_packages {
rm $pkg
_print $" ✓ Deleted: ($pkg | path basename)"
}
# Delete metadata
if ($metadata_file | path exists) {
rm $metadata_file
_print $" ✓ Deleted: ($metadata_file | path basename)"
}
_print ""
_print $"✅ Removed package: ($package_name)"
}
# Clean ALL packages and metadata
export def "clean-all-packages" [
--dry-run = false, # Show what would be deleted
--force = false # Skip confirmation prompt
] {
_print "🗑️ Cleaning ALL packages..."
let dist_config = (get-distribution-config)
let pack_path = $dist_config.pack_path
let registry_path = $dist_config.registry_path
if not ($pack_path | path exists) {
_print " No packages directory found"
return
}
# Get all packages
cd $pack_path
let all_packages = (glob *.tar)
if ($all_packages | is-empty) {
_print " No packages to clean"
return
}
# Get all metadata files
cd $registry_path
let all_metadata = (glob *.json)
# Show what will be deleted
_print ""
_print $" Found ($all_packages | length) package\(s\) and ($all_metadata | length) metadata file\(s\)"
if not $dry_run and not $force {
_print ""
let response = (input $"⚠️ Delete ALL packages and metadata? \(y/N\): ")
if ($response | str downcase) != "y" {
_print "❌ Cancelled"
return
}
}
# Delete packages
_print ""
for pkg in $all_packages {
if $dry_run {
_print $" [DRY RUN] Would delete: ($pkg | path basename)"
} else {
cd $pack_path
rm $pkg
_print $" ✓ Deleted: ($pkg | path basename)"
}
}
# Delete metadata
for meta in $all_metadata {
if $dry_run {
_print $" [DRY RUN] Would delete: ($meta | path basename)"
} else {
cd $registry_path
rm $meta
_print $" ✓ Deleted: ($meta | path basename)"
}
}
_print ""
if $dry_run {
_print $"[DRY RUN] Would clean ($all_packages | length) packages and ($all_metadata | length) metadata files"
} else {
_print $"✅ Cleaned ($all_packages | length) packages and ($all_metadata | length) metadata files"
}
}