provisioning/tools/oci-package.nu

415 lines
12 KiB
Plaintext
Raw Permalink Normal View History

2025-10-07 11:12:02 +01:00
# 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)"
}