provisioning/tools/publish_extension.nu

357 lines
11 KiB
Plaintext
Raw Normal View History

2025-10-07 11:12:02 +01:00
#!/usr/bin/env nu
# Extension Publishing Tool
# Publishes extensions to OCI registry with validation and packaging
use ../core/nulib/lib_provisioning/oci/client.nu *
use ../core/nulib/lib_provisioning/utils/logger.nu *
# Publish extension to OCI registry
export def main [
extension_path: string # Path to extension directory
--registry: string = "" # OCI registry (default from config)
--namespace: string = "" # Registry namespace (default from config)
--version: string # Extension version
--auth-token: string = "" # Authentication token path
--dry-run # Validate without publishing
--force (-f) # Overwrite existing version
] {
print $"📦 Publishing extension to OCI registry..."
# Load config if registry/namespace not provided
let config = if ($registry | is-empty) or ($namespace | is-empty) {
get-oci-config
} else {
{
registry: $registry
namespace: $namespace
auth_token_path: $auth_token
}
}
let final_registry = if ($registry | is-empty) { $config.registry } else { $registry }
let final_namespace = if ($namespace | is-empty) { $config.namespace } else { $namespace }
let token_path = if ($auth_token | is-empty) { $config.auth_token_path } else { $auth_token }
# 1. Validate extension structure
print "1⃣ Validating extension structure..."
let validation = (validate-extension-structure $extension_path)
if not $validation.valid {
print $"❌ Extension validation failed:"
for error in $validation.errors {
print $" • ($error)"
}
exit 1
}
print " ✅ Extension structure valid"
# 2. Read extension manifest
print "2⃣ Reading extension manifest..."
let manifest_file = ($extension_path | path join "extension.yaml")
if not ($manifest_file | path exists) {
print "❌ extension.yaml not found"
exit 1
}
let manifest = (open $manifest_file | from yaml)
let ext_name = ($manifest.extension.name? | default (error make {msg: "Extension name not found in manifest"}))
let ext_version = if ($version | is-not-empty) {
$version
} else {
$manifest.extension.version? | default (error make {msg: "Extension version not specified"})
}
let ext_type = ($manifest.extension.type? | default "unknown")
print $" 📋 Name: ($ext_name)"
print $" 🏷️ Version: ($ext_version)"
print $" 📦 Type: ($ext_type)"
# 3. Check if version already exists
if not $force {
print "3⃣ Checking if version already exists..."
let token = (load-oci-token $token_path)
let exists = (oci-artifact-exists $final_registry $final_namespace $ext_name $ext_version)
if $exists {
print $"❌ Version ($ext_version) already exists in registry"
print " Use --force to overwrite"
exit 1
}
print " ✅ Version available"
}
# 4. Create OCI artifact
print "4⃣ Creating OCI artifact..."
let artifact_path = (package-extension-as-oci $extension_path $manifest $ext_version)
print $" 📁 Artifact created at: ($artifact_path)"
if $dry_run {
print "🏁 Dry run complete (artifact not published)"
print $" Artifact path: ($artifact_path)"
exit 0
}
# 5. Push to registry
print "5⃣ Pushing to OCI registry..."
let token = (load-oci-token $token_path)
let success = (oci-push-artifact
$artifact_path
$final_registry
$final_namespace
$ext_name
$ext_version
--auth-token $token
)
# Clean up artifact
rm -rf $artifact_path
if $success {
print ""
print $"✅ Successfully published ($ext_name):($ext_version)"
print $" Registry: ($final_registry)"
print $" Namespace: ($final_namespace)"
print $" Pull with: oci://($final_registry)/($final_namespace)/($ext_name):($ext_version)"
} else {
print ""
print "❌ Failed to push artifact to OCI registry"
exit 1
}
}
# Validate extension directory structure
def validate-extension-structure [path: string]: nothing -> record {
let mut errors = []
# Check if path exists
if not ($path | path exists) {
$errors = ($errors | append $"Path does not exist: ($path)")
return {valid: false, errors: $errors}
}
# Check required file: extension.yaml
let manifest_file = ($path | path join "extension.yaml")
if not ($manifest_file | path exists) {
$errors = ($errors | append "Missing required file: extension.yaml")
}
# Validate manifest structure
if ($manifest_file | path exists) {
let manifest = (open $manifest_file | from yaml)
# Check required fields
if ($manifest.extension?.name? | is-empty) {
$errors = ($errors | append "extension.name is required in manifest")
}
if ($manifest.extension?.type? | is-empty) {
$errors = ($errors | append "extension.type is required in manifest")
}
if ($manifest.extension?.version? | is-empty) {
$errors = ($errors | append "extension.version is required in manifest")
}
}
# Check for at least one content directory
let has_kcl = (($path | path join "kcl") | path exists)
let has_scripts = (($path | path join "scripts") | path exists)
let has_templates = (($path | path join "templates") | path exists)
if not ($has_kcl or $has_scripts or $has_templates) {
$errors = ($errors | append "Extension must have at least one of: kcl/, scripts/, templates/")
}
{
valid: ($errors | is-empty)
errors: $errors
}
}
# Package extension as OCI artifact
def package-extension-as-oci [
extension_path: string
manifest: record
version: string
]: nothing -> string {
let temp_dir = (mktemp -d)
print $" 📦 Packaging to: ($temp_dir)"
# Copy extension files
let dirs_to_copy = [
{src: "kcl", desc: "KCL schemas"}
{src: "scripts", desc: "Scripts"}
{src: "templates", desc: "Templates"}
{src: "docs", desc: "Documentation"}
]
for dir_info in $dirs_to_copy {
let src_path = ($extension_path | path join $dir_info.src)
if ($src_path | path exists) {
cp -r $src_path $temp_dir
print $" ✅ Copied ($dir_info.desc)"
}
}
# Copy manifest
let manifest_src = ($extension_path | path join "extension.yaml")
cp $manifest_src $temp_dir
print " ✅ Copied manifest"
# Create OCI config with metadata
let oci_config = {
created: (date now | format date "%Y-%m-%dT%H:%M:%SZ")
architecture: "any"
os: "any"
config: {
Labels: {
"org.opencontainers.image.title": ($manifest.extension.name? | default "unknown")
"org.opencontainers.image.version": $version
"org.opencontainers.image.description": ($manifest.extension.description? | default "")
"org.opencontainers.image.authors": ($manifest.extension.author? | default "")
"provisioning.extension.type": ($manifest.extension.type? | default "unknown")
"provisioning.extension.requires": ($manifest.extension.requires? | default [] | to json)
}
}
annotations: {
"provisioning.extension.type": ($manifest.extension.type? | default "unknown")
"provisioning.extension.name": ($manifest.extension.name? | default "unknown")
"provisioning.extension.version": $version
}
}
# Save OCI config
$oci_config | to json | save $"($temp_dir)/oci-config.json"
print " ✅ Created OCI config"
$temp_dir
}
# List published extensions
export def "list" [
--registry: string = ""
--namespace: string = ""
] {
let config = if ($registry | is-empty) or ($namespace | is-empty) {
get-oci-config
} else {
{registry: $registry, namespace: $namespace}
}
let final_registry = if ($registry | is-empty) { $config.registry } else { $registry }
let final_namespace = if ($namespace | is-empty) { $config.namespace } else { $namespace }
print $"📦 Extensions in ($final_registry)/($final_namespace):"
let artifacts = (oci-list-artifacts $final_registry $final_namespace)
if ($artifacts | is-empty) {
print " (no extensions found)"
return
}
for artifact in $artifacts {
print $" • ($artifact)"
}
}
# Delete published extension
export def "delete" [
extension_name: string
version: string
--registry: string = ""
--namespace: string = ""
--auth-token: string = ""
--force (-f)
] {
let config = if ($registry | is-empty) or ($namespace | is-empty) {
get-oci-config
} else {
{registry: $registry, namespace: $namespace, auth_token_path: $auth_token}
}
let final_registry = if ($registry | is-empty) { $config.registry } else { $registry }
let final_namespace = if ($namespace | is-empty) { $config.namespace } else { $namespace }
let token_path = if ($auth_token | is-empty) { $config.auth_token_path } else { $auth_token }
if not $force {
print $"⚠️ Delete ($extension_name):($version) from ($final_registry)/($final_namespace)?"
let confirm = (input "Type 'yes' to confirm: ")
if $confirm != "yes" {
print "❌ Cancelled"
return
}
}
let token = (load-oci-token $token_path)
let success = (oci-delete-artifact
$final_registry
$final_namespace
$extension_name
$version
--auth-token $token
)
if $success {
print $"✅ Deleted ($extension_name):($version)"
} else {
print "❌ Failed to delete extension"
exit 1
}
}
# Show extension info
export def "info" [
extension_name: string
version: string
--registry: string = ""
--namespace: string = ""
] {
let config = if ($registry | is-empty) or ($namespace | is-empty) {
get-oci-config
} else {
{registry: $registry, namespace: $namespace}
}
let final_registry = if ($registry | is-empty) { $config.registry } else { $registry }
let final_namespace = if ($namespace | is-empty) { $config.namespace } else { $namespace }
let metadata = (get-oci-extension-metadata $extension_name $version {
registry: $final_registry
namespace: $final_namespace
})
if ($metadata | is-empty) {
print $"❌ Extension ($extension_name):($version) not found"
exit 1
}
print $"📦 Extension: ($extension_name):($version)"
print $" Registry: ($final_registry)"
print $" Namespace: ($final_namespace)"
print $" Type: ($metadata.annotations?.\"provisioning.extension.type\"? | default 'unknown')"
print $" Digest: ($metadata.oci_digest)"
print $" Created: ($metadata.created)"
print $" Size: ($metadata.size) bytes"
}
# Helper to get OCI extension metadata
def get-oci-extension-metadata [
extension_name: string
version: string
config: record
]: nothing -> record {
use ../core/nulib/lib_provisioning/extensions/discovery.nu get-oci-extension-metadata
get-oci-extension-metadata $extension_name $version $config
}