ACReaper.psm1

function ACReaper {
    param (
        [switch]$dryRun,
        [string]$registry = "",
        [string]$repository = "",
        [int]$keepLatest = 0,
        [string[]]$protectedTags = @("latest", "stable")
    )

    if (-not $registry) {
        $registry = Read-Host "Please enter the registry name"
    }
    if (-not $repository) {
        $repository = Read-Host "Please enter the repository name"
    }
    if ($keepLatest -eq 0) {
        $keepLatest = Read-Host "Please specify the number of the most recent images to keep"
    }

    $images = az acr manifest list-metadata -r $registry -n $repository --output json | ConvertFrom-Json
    $sortedImages = $images | Sort-Object { [datetime]$_.lastUpdateTime } -Descending
    $imagesWithTags = $sortedImages | Where-Object { $_.tags -and $_.tags.Count -gt 0 }
    $imagesToKeep = $imagesWithTags | Select-Object -First $keepLatest
    $imagesToDeleteInitial = $imagesWithTags | Select-Object -Skip $keepLatest

    $protectedImages = @()
    $imagesToDelete = @()
    $skippedProtectedImages = @()
    foreach ($img in $imagesToDeleteInitial) {
        $isProtected = $false
        foreach ($tag in $img.tags) {
            if ($protectedTags -contains $tag) {
                $isProtected = $true
                break
            }
        }
        if ($isProtected) {
            $skippedProtectedImages += $img
        } else {
            $imagesToDelete += $img
        }
    }

    Write-Output "=== Kept Images (Latest $keepLatest tags) ==="
    $keepCounter = 1
    $totalKeepSize = 0
    foreach ($img in $imagesToKeep) {
        $date = [datetime]$img.lastUpdateTime
        $gbSize = [math]::Round($img.imageSize / 1GB, 2)
        $totalKeepSize += $img.imageSize
        foreach ($tag in $img.tags) {
            if ($protectedTags -contains $tag) {
                Write-Output "$keepCounter. skip protected image: tag=${tag}, size=${gbSize}GB, date=${date}"
            } else {
                Write-Output "$keepCounter. keep image: tag=${tag}, size=${gbSize}GB, date=${date}"
            }
            $keepCounter++
        }
    }

    if ($skippedProtectedImages.Count -gt 0) {
        Write-Output "`n=== Protected Images (skipped from deletion) ==="
        $protCounter = 1
        foreach ($img in $skippedProtectedImages) {
            $date = [datetime]$img.lastUpdateTime
            $gbSize = [math]::Round($img.imageSize / 1GB, 2)
            foreach ($tag in $img.tags) {
                Write-Output "$protCounter. skip protected image: tag=${tag}, size=${gbSize}GB, date=${date}"
                $protCounter++
            }
            $totalKeepSize += $img.imageSize
        }
    }
    $totalKeepSizeGB = [math]::Round($totalKeepSize / 1GB, 2)
    Write-Output "Total kept size: ${totalKeepSizeGB}GB`n"

    if ($dryRun) {
        Write-Output "(dry-run) No images will be deleted. Below are the candidates:"
    } else {
        Write-Output "Deleting images..."
    }
    $deleteCounter = 1
    $totalRemoveSize = 0
    foreach ($img in $imagesToDelete) {
        $date = [datetime]$img.lastUpdateTime
        $gbSize = [math]::Round($img.imageSize / 1GB, 2)
        foreach ($tag in $img.tags) {
            Write-Output "$deleteCounter. remove image: tag=${tag}, size=${gbSize}GB, date=${date}"
            $deleteCounter++
        }
        if (-not $dryRun) {
            foreach ($tag in $img.tags) {
                az acr repository delete --name $registry --image "${repository}:${tag}" --yes
            }
        }
        $totalRemoveSize += $img.imageSize
    }
    $totalRemoveSizeGB = [math]::Round($totalRemoveSize / 1GB, 2)
    if ($dryRun) {
        Write-Output "`nTotal size to be removed (dry-run): ${totalRemoveSizeGB}GB"
        Write-Output "Dry-Run complete!"
    } else {
        Write-Output "`nTotal images removed in size: ${totalRemoveSizeGB}GB"
        Write-Output "Deletion completed!"
    }
}

Export-ModuleMember -Function ACReaper