357 lines
11 KiB
Plaintext
357 lines
11 KiB
Plaintext
#!/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
|
||
}
|