Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
242 changes: 169 additions & 73 deletions Docker/Multi-Arch-Inspector/run.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail

# Parse args for flags
UNTAGGED=false
UNTAGGED_DELETE=false
ARGS=()
for arg in "$@"; do
if [[ "$arg" == "--untagged" ]]; then
UNTAGGED=true
elif [[ "$arg" == "--untagged-delete" ]]; then
UNTAGGED_DELETE=true
Comment on lines +11 to +12
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The --untagged-delete flag can be used without --untagged, but based on lines 11-12 and 294, it will only have an effect when used together with --untagged. Consider either: (1) documenting that --untagged-delete requires --untagged, or (2) adding validation to ensure --untagged-delete is only accepted when --untagged is also specified, providing a clear error message otherwise.

Copilot uses AI. Check for mistakes.
else
ARGS+=("$arg")
fi
done
set -- "${ARGS[@]}"
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential bug: When no non-flag arguments are provided, ARGS[@] will be empty and set -- "${ARGS[@]}" could fail or behave unexpectedly with set -u (which is enabled at line 2). This can cause the script to exit prematurely when run with only flags like ./run.sh --untagged. Consider handling the case when ARGS is empty, e.g., set -- "${ARGS[@]+"${ARGS[@]}"}".

Suggested change
set -- "${ARGS[@]}"
set -- "${ARGS[@]+"${ARGS[@]}"}"

Copilot uses AI. Check for mistakes.

# Usage: ./run.sh <org> <repo> <img>
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The usage comment is outdated and doesn't reflect the current script signature. Based on the code at lines 43-46, the script expects 4 parameters (cloudsmith_url, workspace, repo, img), but the comment says <org> <repo> <img> (only 3 parameters). This should be updated to match the actual implementation or the README documentation which mentions "domain, org, repo and image name".

Suggested change
# Usage: ./run.sh <org> <repo> <img>
# Usage: ./run.sh <domain> <org> <repo> <img>

Copilot uses AI. Check for mistakes.
# Requires: curl, jq
# Auth: export CLOUDSMITH_API_KEY=<your_token>
Expand All @@ -16,6 +31,10 @@ fi
CHECK='✅'; CROSS='❌'; TIMER='⏳'; VULN='☠️'
case ${LC_ALL:-${LC_CTYPE:-$LANG}} in *UTF-8*|*utf8*) : ;; *) CHECK='OK'; CROSS='X' ;; esac

# Table Format
TBL_FMT="| %-20s | %-15s | %-30s | %-10s | %-75s |\n"
SEP_LINE="+----------------------+-----------------+--------------------------------+------------+-----------------------------------------------------------------------------+"

completed() { printf '%s%s%s %s\n' "$GREEN" "$CHECK" "$RESET" "$*"; }
progress() { printf '%s%s%s %s\n' "$YELLOW" "$TIMER" "$RESET" "$*"; }
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Undefined variable $YELLOW used in the progress() function. The color setup at lines 24-28 only defines $GREEN, $RED, and $RESET, but $YELLOW is never defined. This will result in an empty/uncolored output for the progress function.

Copilot uses AI. Check for mistakes.
quarantined() { printf '%s%s%s %s\n' "$ORANGE" "$VULN" "$RESET" "$*"; }
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Undefined variable $ORANGE used in the quarantined() function. The color setup at lines 24-28 only defines $GREEN, $RED, and $RESET, but $ORANGE is never defined. This will result in an empty/uncolored output for the quarantined function.

Copilot uses AI. Check for mistakes.
Expand All @@ -30,7 +49,7 @@ if [[ -z "${CLOUDSMITH_URL}" ]]; then
CLOUDSMITH_URL="https://docker.cloudsmith.io"
fi

# uthorization header
# authorization header
AUTH_HEADER=()
if [[ -n "${CLOUDSMITH_API_KEY:-}" ]]; then
AUTH_HEADER=(-H "Authorization: Bearer ${CLOUDSMITH_API_KEY}")
Expand Down Expand Up @@ -81,10 +100,13 @@ getDigestData () {
' <<< "${MANIFEST_JSON}" | awk 'NF' | sort -u)

if (( ${#ARCHS[@]} == 0 )); then
echo "No architecture data found."
exit 1
# echo "No architecture data found."
# exit 1
ARCHS=("unknown")
fi

local platform="${ARCHS[*]}"

# Get the package data from Cloudsmith API packages list endpoint
getPackageData () {

Expand All @@ -107,56 +129,31 @@ getDigestData () {


# handle the different status's
case "${STATUS[0]}" in
Completed)
echo " |____ Status: ${STATUS[0]} ${CHECK}"
;;

"In Progress")
echo " |____ Status: ${STATUS[0]} ${TIMER}"
;;

Quarantined)
echo " |____ Status: ${STATUS[1]} ${VULN}"
;;

Failed)
echo " |____ Status: ${STATUS[0]} ${FAIL}"
;;

esac

case "${STATUS[1]}" in
Completed)
echo " |____ Status: ${STATUS[1]} ${CHECK}"
;;

"In Progress")
echo " |____ Status: ${STATUS[1]} ${TIMER}"
;;

Quarantined)
echo " |____ Status: ${STATUS[1]} ${VULN}"
;;

Failed)
echo " |____ Status: ${STATUS[1]} ${FAIL}"
;;

esac

local status_display=""
for s in "${STATUS[@]}"; do
case "$s" in
Completed) status_display+="${s} ${CHECK} " ;;
"In Progress") status_display+="${s} ${TIMER} " ;;
Quarantined) status_display+="${s} ${VULN} " ;;
Failed) status_display+="${s} ${CROSS} " ;;
*) status_display+="${s} " ;;
esac
done

local dl=0
if (( ${#DOWNLOADS[@]} == 3 )); then
echo " |____ Downloads: ${DOWNLOADS[1]}"
count=${DOWNLOADS[1]}
totalDownloads=$((totalDownloads+count))
else
echo " |____ Downloads: ${DOWNLOADS[0]}"
dl=${DOWNLOADS[1]}
elif (( ${#DOWNLOADS[@]} > 0 )); then
dl=${DOWNLOADS[0]}
fi

totalDownloads=$((totalDownloads+dl))

printf "$TBL_FMT" "${nTAG}" "${platform}" "${status_display}" "${dl}" "${digest}"
echo "$SEP_LINE"

}

echo " - ${digest}"
echo " - Platform: ${ARCHS}"
getPackageData "${digest}"

}
Expand All @@ -169,15 +166,15 @@ getDockerDigests () {
local totalDownloads=0
API_BASE="https://api.cloudsmith.io/v1/packages/${WORKSPACE}/${REPO}/"

index_digest="$(curl -fsSL "${AUTH_HEADER[@]}" \
-H "Accept: application/vnd.oci.image.manifest.v1+json" \
-o /dev/null \
-w "%header{Docker-Content-Digest}" \
"${CLOUDSMITH_URL}/v2/${WORKSPACE}/${REPO}/${IMG}/manifests/${nTAG}")"
# index_digest="$(curl -fsSL "${AUTH_HEADER[@]}" \
# -H "Accept: application/vnd.oci.image.manifest.v1+json" \
# -o /dev/null \
# -w "%header{Docker-Content-Digest}" \
# "${CLOUDSMITH_URL}/v2/${WORKSPACE}/${REPO}/${IMG}/manifests/${nTAG}")"

echo
echo "🐳 ${WORKSPACE}/${REPO}/${IMG}:${nTAG}"
echo " Index Digest: ${index_digest}"
# echo
# echo "🐳 ${WORKSPACE}/${REPO}/${IMG}:${nTAG}"
# echo " Index Digest: ${index_digest}"


MANIFEST_JSON="$(curl -L -sS "${AUTH_HEADER[@]}" \
Expand All @@ -200,34 +197,133 @@ getDockerDigests () {
' <<< "${MANIFEST_JSON}" | awk 'NF' | sort -u)

if (( ${#DIGESTS[@]} == 0 )); then
echo "No digests found."
exit 1
# echo "No digests found."
return
fi

for i in "${!DIGESTS[@]}"; do
echo
getDigestData "${DIGESTS[i]}"
echo
done
echo " |___ Total Downloads: ${totalDownloads}"
# echo " |___ Total Downloads: ${totalDownloads}"

}

getUntaggedImages() {
echo "Searching for untagged manifest lists..."
API_BASE="https://api.cloudsmith.io/v1/packages/${WORKSPACE}/${REPO}/"

# Fetch list
PACKAGES_JSON="$(curl -sS "${AUTH_HEADER[@]}" \
-H "Cache-Control: no-cache" \
--get "${API_BASE}" \
--data-urlencode "query=name:${IMG}")"

# Filter for untagged manifest lists
mapfile -t UNTAGGED_PKGS < <(jq -r '
.[]
| select(.type_display == "manifest/list")
| select(.tags.version == null or (.tags.version | length == 0))
| [ .version, .status_str, (.downloads // 0), .slug ] | @tsv
' <<< "${PACKAGES_JSON}")

if (( ${#UNTAGGED_PKGS[@]} == 0 )); then
echo "No untagged manifest lists found."
return
fi

echo
echo "$SEP_LINE"
printf "$TBL_FMT" "TAG" "PLATFORM" "STATUS" "DOWNLOADS" "DIGEST"
echo "$SEP_LINE"

for pkg in "${UNTAGGED_PKGS[@]}"; do
IFS=$'\t' read -r digest status downloads slug <<< "$pkg"

# Ensure digest has sha256: prefix
if [[ "$digest" != sha256:* ]]; then
digest="sha256:${digest}"
fi

# Fetch manifest to get platforms
MANIFEST_JSON="$(curl -L -sS "${AUTH_HEADER[@]}" \
-H "Accept: application/vnd.oci.image.manifest.v1+json" \
-H "Cache-Control: no-cache" \
"${CLOUDSMITH_URL}/v2/${WORKSPACE}/${REPO}/${IMG}/manifests/${digest}")"

mapfile -t ARCHS < <(jq -r '
if .manifests then
.manifests[] | ((.platform.os // "linux") + "/" + (.platform.architecture // "unknown"))
else
"unknown"
end
' <<< "${MANIFEST_JSON}" | sort -u)

platform="${ARCHS[*]}"

# Format status
local status_display=""
case "$status" in
Completed) status_display+="${status} ${CHECK} " ;;
"In Progress") status_display+="${status} ${TIMER} " ;;
Quarantined) status_display+="${status} ${VULN} " ;;
Failed) status_display+="${status} ${CROSS} " ;;
*) status_display+="${status} " ;;
esac

# Print Parent (Manifest List)
printf "$TBL_FMT" "(untagged) [List]" "${platform}" "${status_display}" "${downloads}" "${digest}"
echo "$SEP_LINE"

# Fetch and Print Children
mapfile -t DIGESTS < <(jq -r '
if type=="object" and (.manifests? // empty and (.manifests[].platform.architecture )) then
.manifests[]?
| select((.platform.architecture? // "unknown") | ascii_downcase != "unknown")
| .digest
else
.. | objects | .digest? // empty
end
' <<< "${MANIFEST_JSON}" | awk 'NF' | sort -u)

local nTAG="(untagged)"
local totalDownloads=0
for i in "${!DIGESTS[@]}"; do
getDigestData "${DIGESTS[i]}"
done

Comment on lines +291 to +293
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue with variable scope: The totalDownloads variable is declared as local at line 289 but is never used or displayed. Unlike in the getDockerDigests function (line 207, commented out), there's no output of the total downloads for untagged images. This creates an inconsistency in the output format between tagged and untagged images.

Suggested change
getDigestData "${DIGESTS[i]}"
done
# getDigestData should echo the downloads as its last output, so we capture it
digest_downloads=$(getDigestData "${DIGESTS[i]}")
totalDownloads=$((totalDownloads + digest_downloads))
done
# Print total downloads for this untagged image (for consistency)
echo "Total downloads for (untagged) [List]: $totalDownloads"

Copilot uses AI. Check for mistakes.
if $UNTAGGED_DELETE; then
echo " Deleting package: ${slug}..."
curl -sS -X DELETE "${AUTH_HEADER[@]}" \
"https://api.cloudsmith.io/v1/packages/${WORKSPACE}/${REPO}/${slug}/"
echo " Deleted."
Comment on lines +296 to +298
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deletion operation at lines 294-300 lacks error handling. If the DELETE request fails (e.g., due to authentication issues, network problems, or cascading feature flag not enabled), the script will still print "Deleted." misleadingly. Consider checking the HTTP response code and providing appropriate error messages. The PR description mentions this "requires the cascading feature flag" but there's no check or user-friendly error message if this requirement isn't met.

Suggested change
curl -sS -X DELETE "${AUTH_HEADER[@]}" \
"https://api.cloudsmith.io/v1/packages/${WORKSPACE}/${REPO}/${slug}/"
echo " Deleted."
# Perform DELETE and capture HTTP status and response
response=$(curl -sS -w "%{http_code}" -o /tmp/delete_response.txt -X DELETE "${AUTH_HEADER[@]}" \
"https://api.cloudsmith.io/v1/packages/${WORKSPACE}/${REPO}/${slug}/")
http_code="${response: -3}"
body=$(cat /tmp/delete_response.txt)
if [[ "$http_code" =~ ^2 ]]; then
echo " Deleted."
else
echo " Failed to delete package: HTTP $http_code"
if [[ "$http_code" == "403" ]] && grep -qi "cascading" <<< "$body"; then
echo " Error: Cascading delete feature flag is not enabled for this repository."
echo " Please enable the cascading feature flag to allow deletion."
else
echo " Response: $body"
fi
fi

Copilot uses AI. Check for mistakes.
echo "$SEP_LINE"
fi
done
}


# Lookup Docker multi-arch images and output an overview
getDockerTags
read -r -a images <<< "$nTAGS"
echo "Found matching tags:"
echo
for t in "${!images[@]}"; do
tag=" - ${images[t]}"
echo "$tag"
done
if $UNTAGGED; then
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing validation: When --untagged flag is used, the script doesn't validate that required parameters (WORKSPACE, REPO, IMG) are provided. If these are empty, the API calls will fail with unclear error messages. Consider adding validation similar to the non-untagged path or providing clearer error messages.

Suggested change
if $UNTAGGED; then
if $UNTAGGED; then
# Validate required parameters for untagged mode
if [[ -z "${WORKSPACE:-}" || -z "${REPO:-}" || -z "${IMG:-}" ]]; then
echo "Error: WORKSPACE, REPO, and IMG must be set when using --untagged." >&2
exit 1
fi

Copilot uses AI. Check for mistakes.
getUntaggedImages
else
getDockerTags
read -r -a images <<< "$nTAGS"
echo "Found matching tags: ${#images[@]}"

echo
for t in "${!images[@]}"; do
getDockerDigests "${images[t]}"
done
for t in "${!images[@]}"; do
tag=" - ${images[t]}"
echo "$tag"
done

echo
echo "$SEP_LINE"
printf "$TBL_FMT" "TAG" "PLATFORM" "STATUS" "DOWNLOADS" "DIGEST"
echo "$SEP_LINE"

for t in "${!images[@]}"; do
getDockerDigests "${images[t]}"
done
fi



Expand Down
Loading