# OCI Registry Client # Handles OCI artifact operations (pull, push, list, search) use ../config/accessor.nu * use ../utils/logging.nu * # OCI client configuration export def get-oci-config []: nothing -> record { { registry: (get-config-value "oci.registry" "localhost:5000") namespace: (get-config-value "oci.namespace" "provisioning-extensions") auth_token_path: (get-config-value "oci.auth_token_path" ($env.HOME | path join ".provisioning" "oci-token")) insecure: (get-config-value "oci.insecure" false) timeout: (get-config-value "oci.timeout" 300) retry_count: (get-config-value "oci.retry_count" 3) } } # Load OCI authentication token export def load-oci-token [token_path: string]: nothing -> string { if ($token_path | path exists) { open $token_path | str trim } else { "" } } # Build OCI artifact reference export def build-artifact-ref [ registry: string namespace: string name: string version: string ]: nothing -> string { $"($registry)/($namespace)/($name):($version)" } # Helper to download OCI layers def download-oci-layers [ layers: list registry: string namespace: string name: string dest_path: string auth_token: string ]: nothing -> bool { for layer in $layers { let blob_url = $"http://($registry)/v2/($namespace)/($name)/blobs/($layer.digest)" let layer_file = $"($dest_path)/($layer.digest | str replace ':' '_').tar.gz" log-debug $"Downloading layer: ($layer.digest)" # Download blob let download_cmd = if ($auth_token | is-not-empty) { $"curl -H 'Authorization: Bearer ($auth_token)' -L -o ($layer_file) ($blob_url)" } else { $"curl -L -o ($layer_file) ($blob_url)" } let result = (do { ^bash -c $download_cmd } | complete) if $result.exit_code != 0 { log-error $"Failed to download layer: ($layer.digest)" return false } # Extract layer log-debug $"Extracting layer: ($layer.digest)" tar -xzf $layer_file -C $dest_path rm $layer_file } true } # Pull OCI artifact using curl and tar export def oci-pull-artifact [ registry: string namespace: string name: string version: string dest_path: string --auth-token: string = "" ]: nothing -> bool { let result = (do { log-info $"Pulling OCI artifact: ($name):($version) from ($registry)/($namespace)" # Create destination directory mkdir $dest_path # Build manifest URL let manifest_url = $"http://($registry)/v2/($namespace)/($name)/manifests/($version)" # Build auth header let auth_header = if ($auth_token | is-not-empty) { ["Authorization" $"Bearer ($auth_token)"] } else { [] } # Fetch manifest log-debug $"Fetching manifest from ($manifest_url)" let manifest_result = (http get --headers $auth_header $manifest_url) if ($manifest_result | is-empty) { log-error "Failed to fetch OCI manifest" error make {msg: "Failed to fetch OCI manifest"} } # Parse manifest let manifest = ($manifest_result | from json) # Save manifest $manifest | to json | save $"($dest_path)/oci-manifest.json" # Download each layer let layers = ($manifest | get layers) let download_result = (download-oci-layers $layers $registry $namespace $name $dest_path $auth_token) if not $download_result { error make {msg: "Failed to download layers"} } log-info $"Successfully pulled ($name):($version)" true } | complete) if $result.exit_code == 0 { $result.stdout } else { log-error $"Failed to pull OCI artifact: ($result.stderr)" false } } # Push OCI artifact using curl export def oci-push-artifact [ artifact_path: string registry: string namespace: string name: string version: string --auth-token: string = "" ]: nothing -> bool { let result = (do { log-info $"Pushing OCI artifact: ($name):($version) to ($registry)/($namespace)" # Create tarball of artifact let temp_tarball = (mktemp --suffix .tar.gz) log-debug $"Creating artifact tarball: ($temp_tarball)" tar -czf $temp_tarball -C $artifact_path . # Calculate digest let digest = (open $temp_tarball | hash sha256) let blob_digest = $"sha256:($digest)" # Upload blob let blob_url = $"http://($registry)/v2/($namespace)/($name)/blobs/uploads/" log-debug $"Uploading blob to ($blob_url)" # Start upload let auth_header = if ($auth_token | is-not-empty) { $"-H 'Authorization: Bearer ($auth_token)'" } else { "" } let start_upload = (do { ^bash -c $"curl -X POST ($auth_header) ($blob_url)" } | complete) if $start_upload.exit_code != 0 { log-error "Failed to start blob upload" rm $temp_tarball error make {msg: "Failed to start blob upload"} } # Extract upload URL from Location header let upload_url = ($start_upload.stdout | str trim) # Upload blob let upload_cmd = $"curl -X PUT ($auth_header) -H 'Content-Type: application/octet-stream' --data-binary @($temp_tarball) '($upload_url)?digest=($blob_digest)'" let upload_result = (do { ^bash -c $upload_cmd } | complete) if $upload_result.exit_code != 0 { log-error "Failed to upload blob" rm $temp_tarball error make {msg: "Failed to upload blob"} } # Create manifest let config = if ($"($artifact_path)/oci-config.json" | path exists) { open $"($artifact_path)/oci-config.json" | from json } else { { created: (date now | format date "%Y-%m-%dT%H:%M:%SZ") architecture: "any" os: "any" } } let manifest = { schemaVersion: 2 mediaType: "application/vnd.oci.image.manifest.v1+json" config: { mediaType: "application/vnd.oci.image.config.v1+json" size: (ls $temp_tarball | get size | get 0) digest: $blob_digest } layers: [ { mediaType: "application/vnd.oci.image.layer.v1.tar+gzip" size: (ls $temp_tarball | get size | get 0) digest: $blob_digest } ] } # Upload manifest let manifest_url = $"http://($registry)/v2/($namespace)/($name)/manifests/($version)" let manifest_json = ($manifest | to json) log-debug $"Uploading manifest to ($manifest_url)" let manifest_cmd = $"curl -X PUT ($auth_header) -H 'Content-Type: application/vnd.oci.image.manifest.v1+json' -d '($manifest_json)' ($manifest_url)" let manifest_result = (do { ^bash -c $manifest_cmd } | complete) if $manifest_result.exit_code != 0 { log-error "Failed to upload manifest" rm $temp_tarball error make {msg: "Failed to upload manifest"} } rm $temp_tarball log-info $"Successfully pushed ($name):($version)" true } | complete) if $result.exit_code == 0 { $result.stdout } else { log-error $"Failed to push OCI artifact: ($result.stderr)" false } } # List artifacts in OCI registry export def oci-list-artifacts [ registry: string namespace: string --auth-token: string = "" ]: nothing -> list { let result = (do { let catalog_url = $"http://($registry)/v2/($namespace)/_catalog" let auth_header = if ($auth_token | is-not-empty) { ["Authorization" $"Bearer ($auth_token)"] } else { [] } let http_result = (http get --headers $auth_header $catalog_url) if ($http_result | is-empty) { [] } else { let catalog = ($http_result | from json) $catalog.repositories? | default [] } } | complete) if $result.exit_code == 0 { $result.stdout } else { log-error $"Failed to list OCI artifacts: ($result.stderr)" [] } } # Get artifact tags from OCI registry export def oci-get-artifact-tags [ registry: string namespace: string name: string --auth-token: string = "" ]: nothing -> list { let result = (do { let tags_url = $"http://($registry)/v2/($namespace)/($name)/tags/list" let auth_header = if ($auth_token | is-not-empty) { ["Authorization" $"Bearer ($auth_token)"] } else { [] } let http_result = (http get --headers $auth_header $tags_url) if ($http_result | is-empty) { [] } else { let tags_data = ($http_result | from json) $tags_data.tags? | default [] } } | complete) if $result.exit_code == 0 { $result.stdout } else { log-error $"Failed to get artifact tags: ($result.stderr)" [] } } # Get artifact manifest from OCI registry export def oci-get-artifact-manifest [ registry: string namespace: string name: string version: string --auth-token: string = "" ]: nothing -> record { let result = (do { let manifest_url = $"http://($registry)/v2/($namespace)/($name)/manifests/($version)" let auth_header = if ($auth_token | is-not-empty) { ["Authorization" $"Bearer ($auth_token)"] } else { [] } let http_result = (http get --headers $auth_header $manifest_url) if ($http_result | is-empty) { {} } else { $http_result | from json } } | complete) if $result.exit_code == 0 { $result.stdout } else { log-error $"Failed to get artifact manifest: ($result.stderr)" {} } } # Check if artifact exists in OCI registry export def oci-artifact-exists [ registry: string namespace: string name: string version?: string ]: nothing -> bool { let result = (do { let artifacts = (oci-list-artifacts $registry $namespace) if ($version | is-empty) { # Just check if artifact name exists $name in $artifacts } else { # Check specific version if $name not-in $artifacts { false } else { let tags = (oci-get-artifact-tags $registry $namespace $name) $version in $tags } } } | complete) if $result.exit_code == 0 { $result.stdout } else { false } } # Delete artifact from OCI registry export def oci-delete-artifact [ registry: string namespace: string name: string version: string --auth-token: string = "" ]: nothing -> bool { let result = (do { log-warn $"Deleting OCI artifact: ($name):($version)" # Get manifest to get digest let manifest = (oci-get-artifact-manifest $registry $namespace $name $version --auth-token $auth_token) if ($manifest | is-empty) { log-error "Manifest not found" error make {msg: "Manifest not found"} } let digest = ($manifest | get config.digest) # Delete manifest let manifest_url = $"http://($registry)/v2/($namespace)/($name)/manifests/($digest)" let auth_header = if ($auth_token | is-not-empty) { $"-H 'Authorization: Bearer ($auth_token)'" } else { "" } let delete_cmd = $"curl -X DELETE ($auth_header) ($manifest_url)" let delete_result = (do { ^bash -c $delete_cmd } | complete) if $delete_result.exit_code == 0 { log-info $"Successfully deleted ($name):($version)" true } else { log-error $"Failed to delete artifact: ($delete_result.stderr)" false } } | complete) if $result.exit_code == 0 { $result.stdout } else { log-error $"Failed to delete OCI artifact: ($result.stderr)" false } } # Check if OCI registry is available export def is-oci-available []: nothing -> bool { let result = (do { let config = (get-oci-config) let health_url = $"http://($config.registry)/v2/" let health_result = (do { http get $health_url } | complete) $health_result.exit_code == 0 } | complete) if $result.exit_code == 0 { $result.stdout } else { false } } # Test OCI connectivity and authentication export def test-oci-connection []: nothing -> record { let config = (get-oci-config) let token = (load-oci-token $config.auth_token_path) mut results = { registry_reachable: false authentication_valid: false catalog_accessible: false errors: [] } # Test registry reachability let health_url = $"http://($config.registry)/v2/" let health_result = (do { http get $health_url } | complete) if $health_result.exit_code == 0 { $results.registry_reachable = true } else { $results.errors = ($results.errors | append "Registry unreachable") } # Test authentication (if token provided) if ($token | is-not-empty) { let catalog = (oci-list-artifacts $config.registry $config.namespace --auth-token $token) if ($catalog | is-not-empty) { $results.authentication_valid = true $results.catalog_accessible = true } else { $results.errors = ($results.errors | append "Authentication failed or catalog empty") } } $results }