# OCI Packaging Tool # Package extensions as OCI artifacts for distribution # Version: 1.0.0 use ../core/nulib/lib_provisioning/config/loader.nu get-config use ../core/nulib/lib_provisioning/oci/client.nu * use std log # Package extension as OCI artifact export def package-extension [ extension_path: string # Path to extension directory --output-path: string # Output path for OCI artifact (optional) --validate # Validate before packaging ] -> string { log info $"Packaging extension from ($extension_path)" # Validate extension structure if $validate { let validation = (validate-extension $extension_path) if not $validation.valid { error make { msg: $"Extension validation failed: ($validation.errors)" } } } # Read extension manifest let manifest_path = ($extension_path | path join "manifest.yaml") if not ($manifest_path | path exists) { error make { msg: $"Missing manifest.yaml in ($extension_path)" } } let manifest = (open $manifest_path | from yaml) # Create temporary directory for artifact let temp_dir = (mktemp -d -t "oci-pkg-XXXXXX") let artifact_dir = ($temp_dir | path join $manifest.name) mkdir $artifact_dir # Copy extension files to artifact directory log info "Copying extension files..." # Required directories let required_dirs = ["kcl", "scripts"] for dir in $required_dirs { let src = ($extension_path | path join $dir) if ($src | path exists) { cp -r $src ($artifact_dir | path join $dir) } } # Optional directories let optional_dirs = ["templates", "docs", "tests"] for dir in $optional_dirs { let src = ($extension_path | path join $dir) if ($src | path exists) { cp -r $src ($artifact_dir | path join $dir) } } # Copy manifest cp $manifest_path ($artifact_dir | path join "manifest.yaml") # Create OCI manifest let oci_manifest = (create-oci-manifest $artifact_dir $manifest) # Save OCI manifest $oci_manifest | to json | save -f ($artifact_dir | path join "oci-manifest.json") # Create tarball let output_tar = if ($output_path | is-not-empty) { $output_path } else { $"($manifest.name)-($manifest.version).tar.gz" } log info $"Creating tarball: ($output_tar)" cd $temp_dir tar czf $output_tar $manifest.name # Move tarball to current directory let final_path = ($env.PWD | path join ($output_tar | path basename)) mv $output_tar $final_path # Cleanup rm -rf $temp_dir log info $"✓ Package created: ($final_path)" $final_path } # Validate extension structure before packaging export def validate-extension [ extension_path: string ] -> record { log info $"Validating extension at ($extension_path)" mut errors = [] mut warnings = [] # Check if directory exists if not ($extension_path | path exists) { $errors = ($errors | append "Extension directory does not exist") return { valid: false errors: $errors warnings: $warnings } } # Check for manifest.yaml let manifest_path = ($extension_path | path join "manifest.yaml") if not ($manifest_path | path exists) { $errors = ($errors | append "Missing manifest.yaml") } else { # Validate manifest content try { let manifest = (open $manifest_path | from yaml) # Required fields let required_fields = ["name", "type", "version"] for field in $required_fields { if ($manifest | get -i $field | is-empty) { $errors = ($errors | append $"Missing required field in manifest: ($field)") } } # Validate type let valid_types = ["provider", "taskserv", "cluster"] if not ($manifest.type in $valid_types) { $errors = ($errors | append $"Invalid type in manifest: ($manifest.type)") } # Validate version format (basic semver check) if ($manifest.version? | is-not-empty) { if ($manifest.version | str contains ".") == false { $warnings = ($warnings | append "Version should follow semver format (x.y.z)") } } } catch { |err| $errors = ($errors | append $"Invalid manifest.yaml: ($err.msg)") } } # Check for required directories let kcl_dir = ($extension_path | path join "kcl") if not ($kcl_dir | path exists) { $errors = ($errors | append "Missing kcl/ directory") } else { # Check for kcl.mod let kcl_mod = ($kcl_dir | path join "kcl.mod") if not ($kcl_mod | path exists) { $warnings = ($warnings | append "Missing kcl/kcl.mod (recommended)") } # Check for at least one .k file let k_files = (ls $kcl_dir | where name =~ "\.k$") if ($k_files | is-empty) { $errors = ($errors | append "No .k files found in kcl/ directory") } } # Check for scripts directory let scripts_dir = ($extension_path | path join "scripts") if not ($scripts_dir | path exists) { $warnings = ($warnings | append "Missing scripts/ directory (recommended)") } else { # Check for install.nu let install_script = ($scripts_dir | path join "install.nu") if not ($install_script | path exists) { $warnings = ($warnings | append "Missing scripts/install.nu (recommended)") } } # Check for optional but recommended directories let docs_dir = ($extension_path | path join "docs") if not ($docs_dir | path exists) { $warnings = ($warnings | append "Missing docs/ directory (recommended)") } let readme = ($extension_path | path join "README.md") if not ($readme | path exists) { $warnings = ($warnings | append "Missing README.md (recommended)") } { valid: ($errors | is-empty) errors: $errors warnings: $warnings } } # Create OCI manifest for extension export def create-oci-manifest [ artifact_path: string extension_manifest: record ] -> record { log info "Creating OCI manifest..." # Calculate layer sizes let layers = (ls -a $artifact_path | each { |item| { mediaType: "application/vnd.oci.image.layer.v1.tar+gzip" size: $item.size digest: $"sha256:(echo $item.name | hash sha256)" annotations: { "org.opencontainers.image.title": $item.name } } }) # Create config let config = { mediaType: "application/vnd.kcl.package.config.v1+json" size: 0 # Will be calculated digest: "" # Will be calculated data: $extension_manifest } # Create manifest { schemaVersion: 2 mediaType: "application/vnd.oci.image.manifest.v1+json" config: $config layers: $layers annotations: { "org.opencontainers.image.created": (date now | format date "%Y-%m-%dT%H:%M:%SZ") "org.opencontainers.image.title": $extension_manifest.name "org.opencontainers.image.description": ($extension_manifest.description? | default "") "org.opencontainers.image.version": $extension_manifest.version "org.opencontainers.image.authors": ($extension_manifest.author? | default "") "org.opencontainers.image.licenses": ($extension_manifest.license? | default "MIT") "provisioning.extension.type": $extension_manifest.type } } } # Build and push extension to OCI registry export def publish-extension [ extension_path: string registry: string namespace: string --version: string # Override version from manifest --insecure --validate ] -> bool { log info $"Publishing extension from ($extension_path)" # Validate extension if $validate { let validation = (validate-extension $extension_path) if not $validation.valid { log error $"Validation failed: ($validation.errors)" return false } if not ($validation.warnings | is-empty) { log warning $"Validation warnings:" for warn in $validation.warnings { log warning $" - ($warn)" } } } # Read manifest let manifest_path = ($extension_path | path join "manifest.yaml") let manifest = (open $manifest_path | from yaml) let ext_version = if ($version | is-not-empty) { $version } else { $manifest.version } log info $"Publishing ($manifest.name):($ext_version) to ($registry)/($namespace)" # Push to OCI registry let result = (push-artifact $extension_path $registry $namespace $manifest.name $ext_version --insecure=$insecure) if $result { log info $"✓ Successfully published ($manifest.name):($ext_version)" log info $" Reference: ($registry)/($namespace)/($manifest.name):($ext_version)" true } else { log error $"Failed to publish ($manifest.name):($ext_version)" false } } # Unpack OCI artifact to local directory export def unpack-extension [ artifact_path: string # Path to .tar.gz OCI artifact destination: string # Destination directory ] -> string { log info $"Unpacking OCI artifact from ($artifact_path)" if not ($artifact_path | path exists) { error make { msg: $"Artifact not found: ($artifact_path)" } } # Create destination directory mkdir $destination # Extract tarball log info $"Extracting to ($destination)" tar xzf $artifact_path -C $destination # Find the extension directory (should be only one) let ext_dirs = (ls $destination | where type == "dir") if ($ext_dirs | length) != 1 { error make { msg: "Artifact should contain exactly one directory" } } let ext_path = ($ext_dirs | first | get name) log info $"✓ Extension unpacked to ($ext_path)" $ext_path } # List contents of OCI artifact without extracting export def inspect-artifact [ artifact_path: string ] -> record { log info $"Inspecting OCI artifact: ($artifact_path)" if not ($artifact_path | path exists) { error make { msg: $"Artifact not found: ($artifact_path)" } } # List tarball contents let contents = (tar tzf $artifact_path | lines) # Try to extract and read manifest let temp_dir = (mktemp -d -t "oci-inspect-XXXXXX") tar xzf $artifact_path -C $temp_dir # Find manifest.yaml let manifest_files = (ls $temp_dir/**/manifest.yaml -r | where type == "file") let manifest = if not ($manifest_files | is-empty) { let manifest_path = ($manifest_files | first | get name) open $manifest_path | from yaml } else { {} } # Cleanup rm -rf $temp_dir { artifact: ($artifact_path | path basename) size: (ls $artifact_path | first | get size) files: $contents manifest: $manifest } } # Generate manifest template for extension export def generate-manifest [ extension_name: string extension_type: string # provider, taskserv, cluster --version: string = "0.1.0" --description: string --author: string ] -> record { { name: $extension_name type: $extension_type version: $version description: ($description | default $"($extension_type) extension for ($extension_name)") author: ($author | default $env.USER) license: "MIT" platforms: ["linux/amd64"] dependencies: {} tags: [] } } # Save manifest template to file export def "generate-manifest save" [ extension_name: string extension_type: string output_path: string --version: string = "0.1.0" --description: string --author: string ] -> nothing { let manifest = (generate-manifest $extension_name $extension_type --version $version --description $description --author $author) $manifest | to yaml | save -f $output_path log info $"✓ Manifest template saved to ($output_path)" }