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
|
|||
|
|
}
|