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

357 lines
11 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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
}