پاک کردن ایمیج های اضافی

برای پاک کردن ایمیج‌های قابل حذف ابتدا دستور زیر را اجرا کنید:

cat > /opt/registry-retention/delete-registry-candidates.sh <<'EOF'
#!/usr/bin/env bash

set -euo pipefail

SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${CONFIG_FILE:-${SCRIPT_DIR}/registry-retention.conf}"

if [[ ! -f "$CONFIG_FILE" ]]; then
    echo "ERROR: Configuration file not found: $CONFIG_FILE" >&2
    exit 1
fi

# shellcheck source=/dev/null
source "$CONFIG_FILE"

required_variables=(
    REGISTRY_SCHEME
    REGISTRY_ADDRESS
    REPORT_FILE
)

for variable_name in "${required_variables[@]}"; do
    if [[ -z "${!variable_name:-}" ]]; then
        echo "ERROR: Required configuration is empty: ${variable_name}" >&2
        exit 1
    fi
done

for command_name in curl awk sort; do
    if ! command -v "$command_name" >/dev/null 2>&1; then
        echo "ERROR: Required command not found: ${command_name}" >&2
        exit 1
    fi
done

REGISTRY_URL="${REGISTRY_SCHEME}://${REGISTRY_ADDRESS}"
REPORT="$REPORT_FILE"

if [[ ! -f "$REPORT" ]]; then
    echo "ERROR: Report file not found: $REPORT" >&2
    echo "Run registry-retention-dry-run.sh first." >&2
    exit 1
fi

expected_header=$'action\trepository\ttag\tpushed_epoch\tpushed_at\tdigest'
actual_header="$(head -n 1 "$REPORT")"

if [[ "$actual_header" != "$expected_header" ]]; then
    echo "ERROR: Unexpected report format." >&2
    echo "Expected:" >&2
    printf '%s\n' "$expected_header" >&2
    echo "Found:" >&2
    printf '%s\n' "$actual_header" >&2
    exit 1
fi

AUTH_ARGS=()

if [[ -n "${REGISTRY_USERNAME:-}" ]]; then
    AUTH_ARGS=(
        --user "${REGISTRY_USERNAME}:${REGISTRY_PASSWORD:-}"
    )
fi

candidate_tag_count="$(
    awk -F'\t' '
        NR > 1 && $1 == "DELETE-CANDIDATE" {
            count++
        }
        END {
            print count + 0
        }
    ' "$REPORT"
)"

candidate_digest_count="$(
    awk -F'\t' '
        NR > 1 &&
        $1 == "DELETE-CANDIDATE" &&
        $6 != "" &&
        $6 != "unknown" {
            print $2 "\t" $6
        }
    ' "$REPORT" |
    sort -u |
    wc -l
)"

if [[ "$candidate_tag_count" -eq 0 ]]; then
    echo "No DELETE-CANDIDATE rows were found."
    echo "Nothing was deleted."
    exit 0
fi

echo "Registry             : ${REGISTRY_URL}"
echo "Report               : ${REPORT}"
echo "Candidate tags       : ${candidate_tag_count}"
echo "Unique delete digests: ${candidate_digest_count}"
echo

echo "The following images are candidates for deletion:"
echo

awk -F'\t' '
    NR > 1 && $1 == "DELETE-CANDIDATE" {
        printf "  %-65s %s\n", $2 ":" $3, $6
    }
' "$REPORT"

echo

if [[ "${CONFIRM:-}" != "YES" ]]; then
    echo "Deletion is disabled."
    echo
    echo "Review the list above, then run:"
    echo "CONFIRM=YES ./delete-registry-candidates.sh"
    exit 1
fi

# Additional confirmation value to prevent accidental execution.
if [[ "${CONFIRM_REGISTRY:-}" != "$REGISTRY_ADDRESS" ]]; then
    echo "ERROR: Registry confirmation does not match." >&2
    echo
    echo "Run with both confirmation variables:" >&2
    echo "CONFIRM=YES CONFIRM_REGISTRY=${REGISTRY_ADDRESS} ./delete-registry-candidates.sh" >&2
    exit 1
fi

declare -A processed=()

deleted_count=0
failed_count=0
duplicate_count=0
invalid_count=0
protected_count=0

while IFS=$'\t' read -r \
    action \
    repository \
    tag \
    pushed_epoch \
    pushed_at \
    digest
do
    [[ "$action" != "DELETE-CANDIDATE" ]] && continue

    if [[ -z "$repository" || -z "$tag" ]]; then
        echo "SKIP invalid row: missing repository or tag"
        invalid_count=$((invalid_count + 1))
        continue
    fi

    if [[ -z "$digest" || "$digest" == "unknown" ]]; then
        echo "SKIP missing digest: ${repository}:${tag}"
        invalid_count=$((invalid_count + 1))
        continue
    fi

    if [[ ! "$digest" =~ ^sha256:[0-9a-fA-F]{64}$ ]]; then
        echo "SKIP invalid digest: ${repository}:${tag} ${digest}"
        invalid_count=$((invalid_count + 1))
        continue
    fi

    key="${repository}|${digest}"

    if [[ -n "${processed[$key]:-}" ]]; then
        echo "SKIP duplicate digest: ${repository}:${tag} ${digest}"
        duplicate_count=$((duplicate_count + 1))
        continue
    fi

    # Safety check:
    # Never delete a digest if the report also marks the same
    # repository+digest as KEEP-LATEST or KEEP-PREVIOUS.
    if awk -F'\t' \
        -v repository="$repository" \
        -v digest="$digest" '
            NR > 1 &&
            $2 == repository &&
            $6 == digest &&
            ($1 == "KEEP-LATEST" || $1 == "KEEP-PREVIOUS") {
                found = 1
                exit
            }
            END {
                exit(found ? 0 : 1)
            }
        ' "$REPORT"
    then
        echo "PROTECTED: ${repository}:${tag} ${digest}"
        echo "Reason: the same digest is marked as KEEP in the report."
        protected_count=$((protected_count + 1))
        processed["$key"]=1
        continue
    fi

    processed["$key"]=1

    status="$(
        curl \
            "${AUTH_ARGS[@]}" \
            --silent \
            --show-error \
            --output /dev/null \
            --write-out '%{http_code}' \
            --request DELETE \
            "${REGISTRY_URL}/v2/${repository}/manifests/${digest}" ||
        true
    )"

    case "$status" in
        202)
            echo "DELETED: ${repository}:${tag} ${digest}"
            deleted_count=$((deleted_count + 1))
            ;;

        404)
            echo "NOT FOUND: ${repository}:${tag} ${digest}"
            echo "The manifest may already have been deleted."
            failed_count=$((failed_count + 1))
            ;;

        405)
            echo "FAILED: ${repository}:${tag} HTTP=405"
            echo "Manifest deletion is not enabled in Registry configuration."
            failed_count=$((failed_count + 1))
            ;;

        *)
            echo "FAILED: ${repository}:${tag} HTTP=${status:-curl-error} digest=${digest}"
            failed_count=$((failed_count + 1))
            ;;
    esac

done < <(tail -n +2 "$REPORT")

echo
echo "============================================================"
echo "Registry manifest deletion completed"
echo "============================================================"
echo "Deleted unique digests : ${deleted_count}"
echo "Failed deletions       : ${failed_count}"
echo "Duplicate report rows  : ${duplicate_count}"
echo "Invalid report rows    : ${invalid_count}"
echo "Protected digests      : ${protected_count}"
echo
echo "Manifest deletion does not immediately release disk space."
echo "Run Registry garbage collection after reviewing the result."
EOF

chmod +x /opt/registry-retention/delete-registry-candidates.sh

برای تست ابتدا به صورت امتحانی این دستور را اجرا میکنیم:

./delete-registry-candidates.sh

خروجی قابل انتظار:

[root@node1 registry-retention]# ./delete-registry-candidates.sh
Registry             : http://10.10.10.99:5000
Report               : /opt/registry-retention/registry-retention-report.tsv
Candidate tags       : 1
Unique delete digests: 1

The following images are candidates for deletion:

  data-foundation-panel-backend:null                                sha256:e5d8350338ebb63631b2c2c5bfa781b3b59981c73e03352a592f8f794b651e3b

Deletion is disabled.

Review the list above, then run:
CONFIRM=YES ./delete-registry-candidates.sh

سپس پس از اطمینان از خروجی دستور زیر را برای پاک کردن نهایی ایمیج ها وارد کنید:

CONFIRM=YES \
CONFIRM_REGISTRY=10.10.10.99:5000 \
./delete-registry-candidates.sh

این دستور فقط manifest هارا پاک میکند. برای پاک کردن blob های بدون manifest باید از garbage collector استفاده کنیم:

docker run --rm \
  -v rke-registry:/var/lib/registry \
  registry:latest \
  garbage-collect --delete-untagged \
  /etc/distribution/config.yml

توجه کنید که مورد اخر این دستور را باید با توجه به تنظیمات registry خود قرار دهید.