327 lines
8.9 KiB
Plaintext
327 lines
8.9 KiB
Plaintext
|
|
#!/usr/bin/env nu
|
|||
|
|
|
|||
|
|
# Migrate images between registries
|
|||
|
|
|
|||
|
|
export def main [
|
|||
|
|
--source-registry: string = "localhost:5000"
|
|||
|
|
--source-type: string = "zot"
|
|||
|
|
--dest-registry: string = "localhost:5001"
|
|||
|
|
--dest-type: string = "harbor"
|
|||
|
|
--namespaces: list<string> = []
|
|||
|
|
--dry-run
|
|||
|
|
--use-skopeo
|
|||
|
|
] {
|
|||
|
|
print "🔄 Migrating registry images..."
|
|||
|
|
print $" Source: ($source_registry) \(($source_type)\)"
|
|||
|
|
print $" Destination: ($dest_registry) \(($dest_type)\)"
|
|||
|
|
|
|||
|
|
if $dry_run {
|
|||
|
|
print " Mode: DRY RUN (no actual migration)"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print ""
|
|||
|
|
|
|||
|
|
# Get source catalog
|
|||
|
|
print "📋 Fetching source catalog..."
|
|||
|
|
let source_repos = get-registry-repos $source_registry
|
|||
|
|
|
|||
|
|
if ($source_repos | is-empty) {
|
|||
|
|
print "❌ No repositories found in source registry"
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print $" Found ($source_repos | length) repositories"
|
|||
|
|
|
|||
|
|
# Filter by namespaces if specified
|
|||
|
|
let repos_to_migrate = if ($namespaces | is-empty) {
|
|||
|
|
$source_repos
|
|||
|
|
} else {
|
|||
|
|
$source_repos | where { |repo|
|
|||
|
|
let ns = ($repo | split row "/" | first)
|
|||
|
|
$ns in $namespaces
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print $" Will migrate ($repos_to_migrate | length) repositories\n"
|
|||
|
|
|
|||
|
|
# Migrate each repository
|
|||
|
|
mut success = 0
|
|||
|
|
mut failed = 0
|
|||
|
|
|
|||
|
|
for repo in $repos_to_migrate {
|
|||
|
|
print $"Migrating: ($repo)"
|
|||
|
|
|
|||
|
|
# Get tags for repository
|
|||
|
|
let tags = get-repo-tags $source_registry $repo
|
|||
|
|
|
|||
|
|
if ($tags | is-empty) {
|
|||
|
|
print " ⚠️ No tags found, skipping"
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print $" Tags: ($tags | str join ', ')"
|
|||
|
|
|
|||
|
|
# Migrate each tag
|
|||
|
|
for tag in $tags {
|
|||
|
|
let source_image = $"($source_registry)/($repo):($tag)"
|
|||
|
|
let dest_image = $"($dest_registry)/($repo):($tag)"
|
|||
|
|
|
|||
|
|
if $dry_run {
|
|||
|
|
print $" [DRY RUN] Would migrate: ($source_image) → ($dest_image)"
|
|||
|
|
$success = ($success + 1)
|
|||
|
|
} else {
|
|||
|
|
if $use_skopeo {
|
|||
|
|
# Use skopeo for migration
|
|||
|
|
if (migrate-with-skopeo $source_image $dest_image) {
|
|||
|
|
print $" ✓ Migrated: ($tag)"
|
|||
|
|
$success = ($success + 1)
|
|||
|
|
} else {
|
|||
|
|
print $" ✗ Failed: ($tag)"
|
|||
|
|
$failed = ($failed + 1)
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
# Use Docker for migration
|
|||
|
|
if (migrate-with-docker $source_image $dest_image) {
|
|||
|
|
print $" ✓ Migrated: ($tag)"
|
|||
|
|
$success = ($success + 1)
|
|||
|
|
} else {
|
|||
|
|
print $" ✗ Failed: ($tag)"
|
|||
|
|
$failed = ($failed + 1)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print ""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Summary
|
|||
|
|
print "=" * 60
|
|||
|
|
print $"Migration Summary:"
|
|||
|
|
print $" Successfully migrated: ($success) images"
|
|||
|
|
print $" Failed: ($failed) images"
|
|||
|
|
|
|||
|
|
if $dry_run {
|
|||
|
|
print "\nℹ This was a dry run. Use without --dry-run to actually migrate."
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if $failed > 0 {
|
|||
|
|
print "\n⚠️ Some migrations failed. Check logs above."
|
|||
|
|
} else {
|
|||
|
|
print "\n✅ All migrations completed successfully!"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def get-registry-repos [registry_url: string] -> list {
|
|||
|
|
let result = (do {
|
|||
|
|
http get $"http://($registry_url)/v2/_catalog"
|
|||
|
|
} | complete)
|
|||
|
|
|
|||
|
|
if $result.exit_code != 0 {
|
|||
|
|
return []
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let catalog = ($result.stdout | from json)
|
|||
|
|
$catalog.repositories
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def get-repo-tags [registry_url: string, repo: string] -> list {
|
|||
|
|
let result = (do {
|
|||
|
|
http get $"http://($registry_url)/v2/($repo)/tags/list"
|
|||
|
|
} | complete)
|
|||
|
|
|
|||
|
|
if $result.exit_code != 0 {
|
|||
|
|
return []
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let tags_data = ($result.stdout | from json)
|
|||
|
|
$tags_data.tags | default []
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def migrate-with-docker [source: string, dest: string] -> bool {
|
|||
|
|
# Pull from source
|
|||
|
|
let pull_result = (^docker pull $source | complete)
|
|||
|
|
if $pull_result.exit_code != 0 {
|
|||
|
|
print $" Error pulling: ($pull_result.stderr)"
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Tag for destination
|
|||
|
|
let tag_result = (^docker tag $source $dest | complete)
|
|||
|
|
if $tag_result.exit_code != 0 {
|
|||
|
|
print $" Error tagging: ($tag_result.stderr)"
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Push to destination
|
|||
|
|
let push_result = (^docker push $dest | complete)
|
|||
|
|
if $push_result.exit_code != 0 {
|
|||
|
|
print $" Error pushing: ($push_result.stderr)"
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Cleanup local image
|
|||
|
|
^docker rmi $source | ignore
|
|||
|
|
^docker rmi $dest | ignore
|
|||
|
|
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def migrate-with-skopeo [source: string, dest: string] -> bool {
|
|||
|
|
# Check if skopeo is available
|
|||
|
|
let skopeo_check = (^skopeo --version | complete)
|
|||
|
|
if $skopeo_check.exit_code != 0 {
|
|||
|
|
print " Error: skopeo not found"
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Use skopeo copy
|
|||
|
|
let copy_result = (^skopeo copy \
|
|||
|
|
$"docker://($source)" \
|
|||
|
|
$"docker://($dest)" \
|
|||
|
|
--insecure-policy | complete)
|
|||
|
|
|
|||
|
|
if $copy_result.exit_code != 0 {
|
|||
|
|
print $" Error: ($copy_result.stderr)"
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Sync specific namespace
|
|||
|
|
export def "sync namespace" [
|
|||
|
|
namespace: string
|
|||
|
|
--source-registry: string = "localhost:5000"
|
|||
|
|
--dest-registry: string = "localhost:5001"
|
|||
|
|
--dry-run
|
|||
|
|
] {
|
|||
|
|
main --source-registry $source_registry \
|
|||
|
|
--dest-registry $dest_registry \
|
|||
|
|
--namespaces [$namespace] \
|
|||
|
|
--dry-run=$dry_run
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Sync all namespaces
|
|||
|
|
export def "sync all" [
|
|||
|
|
--source-registry: string = "localhost:5000"
|
|||
|
|
--dest-registry: string = "localhost:5001"
|
|||
|
|
--dry-run
|
|||
|
|
] {
|
|||
|
|
let namespaces = [
|
|||
|
|
"provisioning-extensions"
|
|||
|
|
"provisioning-kcl"
|
|||
|
|
"provisioning-platform"
|
|||
|
|
"provisioning-test"
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
for ns in $namespaces {
|
|||
|
|
print $"\n🔄 Syncing namespace: ($ns)"
|
|||
|
|
sync namespace $ns --source-registry $source_registry \
|
|||
|
|
--dest-registry $dest_registry \
|
|||
|
|
--dry-run=$dry_run
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Backup registry to tar files
|
|||
|
|
export def "backup to-tar" [
|
|||
|
|
--registry-url: string = "localhost:5000"
|
|||
|
|
--output-dir: string = "./registry-backup"
|
|||
|
|
--namespaces: list<string> = []
|
|||
|
|
] {
|
|||
|
|
print "💾 Backing up registry to tar files..."
|
|||
|
|
print $" Registry: ($registry_url)"
|
|||
|
|
print $" Output: ($output_dir)\n"
|
|||
|
|
|
|||
|
|
# Create output directory
|
|||
|
|
mkdir $output_dir
|
|||
|
|
|
|||
|
|
# Get repositories
|
|||
|
|
let repos = get-registry-repos $registry_url
|
|||
|
|
|
|||
|
|
let repos_to_backup = if ($namespaces | is-empty) {
|
|||
|
|
$repos
|
|||
|
|
} else {
|
|||
|
|
$repos | where { |repo|
|
|||
|
|
let ns = ($repo | split row "/" | first)
|
|||
|
|
$ns in $namespaces
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print $"Backing up ($repos_to_backup | length) repositories...\n"
|
|||
|
|
|
|||
|
|
for repo in $repos_to_backup {
|
|||
|
|
print $"Backing up: ($repo)"
|
|||
|
|
|
|||
|
|
let tags = get-repo-tags $registry_url $repo
|
|||
|
|
|
|||
|
|
for tag in $tags {
|
|||
|
|
let image = $"($registry_url)/($repo):($tag)"
|
|||
|
|
let filename = $"($output_dir)/($repo | str replace '/' '_')_($tag).tar"
|
|||
|
|
|
|||
|
|
# Pull image
|
|||
|
|
^docker pull $image | ignore
|
|||
|
|
|
|||
|
|
# Save to tar
|
|||
|
|
let save_result = (^docker save -o $filename $image | complete)
|
|||
|
|
|
|||
|
|
if $save_result.exit_code == 0 {
|
|||
|
|
print $" ✓ Saved: ($filename)"
|
|||
|
|
} else {
|
|||
|
|
print $" ✗ Failed: ($tag)"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print "\n✅ Backup complete!"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Restore registry from tar files
|
|||
|
|
export def "restore from-tar" [
|
|||
|
|
--input-dir: string = "./registry-backup"
|
|||
|
|
--registry-url: string = "localhost:5000"
|
|||
|
|
] {
|
|||
|
|
print "📥 Restoring registry from tar files..."
|
|||
|
|
print $" Input: ($input_dir)"
|
|||
|
|
print $" Registry: ($registry_url)\n"
|
|||
|
|
|
|||
|
|
# Find all tar files
|
|||
|
|
let tar_files = (ls $"($input_dir)/*.tar" | get name)
|
|||
|
|
|
|||
|
|
print $"Found ($tar_files | length) tar files\n"
|
|||
|
|
|
|||
|
|
for tar_file in $tar_files {
|
|||
|
|
print $"Restoring: ($tar_file)"
|
|||
|
|
|
|||
|
|
# Load from tar
|
|||
|
|
let load_result = (^docker load -i $tar_file | complete)
|
|||
|
|
|
|||
|
|
if $load_result.exit_code != 0 {
|
|||
|
|
print $" ✗ Failed to load"
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Extract image name from docker load output
|
|||
|
|
# Format: "Loaded image: localhost:5000/repo:tag"
|
|||
|
|
let image = ($load_result.stdout | str trim | split row ": " | last)
|
|||
|
|
|
|||
|
|
# Retag if needed and push
|
|||
|
|
let target_image = $"($registry_url)/($image | split row '/' | skip 1 | str join '/')"
|
|||
|
|
|
|||
|
|
^docker tag $image $target_image | ignore
|
|||
|
|
let push_result = (^docker push $target_image | complete)
|
|||
|
|
|
|||
|
|
if $push_result.exit_code == 0 {
|
|||
|
|
print $" ✓ Restored and pushed"
|
|||
|
|
} else {
|
|||
|
|
print $" ✗ Failed to push"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Cleanup
|
|||
|
|
^docker rmi $image | ignore
|
|||
|
|
^docker rmi $target_image | ignore
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print "\n✅ Restore complete!"
|
|||
|
|
}
|