diff --git a/Docker/Multi-Arch-Inspector/README.md b/Docker/Multi-Arch-Inspector/README.md new file mode 100644 index 0000000..254efdf --- /dev/null +++ b/Docker/Multi-Arch-Inspector/README.md @@ -0,0 +1,47 @@ +# Docker Multi-Arch images overview script + +The script will return an overview of your Docker multi-arch images stored in your Cloudsmith repository. +It provides a hierarchial breakdown of each image by tag, showing the index digest and it's associated manifest digests with their platform, cloudsmith sync status and downloads count. + +Each image has a total downloads count rolled up from all digests which the current Cloudsmith UI/ API does not provide. + + + +## Prequisities + +Configure the Cloudsmith environment variable with your PAT or Service Account Token. + + export CLOUDSMITH_API_KEY= + + +## How to use + +Execute run.sh and pass in 4 arguements ( domain, org, repo and image name). + + ./run.sh colinmoynes-test-org docker library/golang + +* if not using a custom domain, you can simply pass in an empty string "" as the first param + + +## So, how does this work? + +## Get matching tags + + * Fetch all tags via the Docker v2 /tags/list endpoint using the image name e.g. library/nginx + + +### For each tag + +* Pass the tag into manifests/ endpoint and return json for the manifest/list file. +* Read the json and parse out the digests +* Total downloads count value incremented from child manifests + +#### For each digest + +* Iterate through the digests +* Fetch the platform and os data from the manifest json +* Lookup the digest (version) via the Cloudsmith packages list endpoint using query string. +* Fetch the sync status and downloads count values +* Increment the total downloads value + + diff --git a/Docker/Multi-Arch-Inspector/example.gif b/Docker/Multi-Arch-Inspector/example.gif new file mode 100644 index 0000000..4992e9e Binary files /dev/null and b/Docker/Multi-Arch-Inspector/example.gif differ diff --git a/Docker/Multi-Arch-Inspector/run.sh b/Docker/Multi-Arch-Inspector/run.sh new file mode 100755 index 0000000..013bf40 --- /dev/null +++ b/Docker/Multi-Arch-Inspector/run.sh @@ -0,0 +1,237 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Usage: ./run.sh +# Requires: curl, jq +# Auth: export CLOUDSMITH_API_KEY= + +# Color setup (auto-disable if not a TTY or tput missing) +if [ -t 1 ] && command -v tput >/dev/null 2>&1 && [ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]; then + GREEN="$(tput setaf 2)"; RED="$(tput setaf 1)"; RESET="$(tput sgr0)" +else + GREEN=""; RED=""; RESET="" +fi + +# Icons (fallback to ASCII if not on UTF-8 locale) +CHECK='✅'; CROSS='❌'; TIMER='⏳'; VULN='☠️' +case ${LC_ALL:-${LC_CTYPE:-$LANG}} in *UTF-8*|*utf8*) : ;; *) CHECK='OK'; CROSS='X' ;; esac + +completed() { printf '%s%s%s %s\n' "$GREEN" "$CHECK" "$RESET" "$*"; } +progress() { printf '%s%s%s %s\n' "$YELLOW" "$TIMER" "$RESET" "$*"; } +quarantined() { printf '%s%s%s %s\n' "$ORANGE" "$VULN" "$RESET" "$*"; } +fail() { printf '%s%s%s %s\n' "$RED" "$CROSS" "$RESET" "$*"; } + +CLOUDSMITH_URL="${1:-}" +WORKSPACE="${2:-}" +REPO="${3:-}" +IMG="${4:-}" + +if [[ -z "${CLOUDSMITH_URL}" ]]; then + CLOUDSMITH_URL="https://docker.cloudsmith.io" +fi + +# uthorization header +AUTH_HEADER=() +if [[ -n "${CLOUDSMITH_API_KEY:-}" ]]; then + AUTH_HEADER=(-H "Authorization: Bearer ${CLOUDSMITH_API_KEY}") +fi + +echo +echo "Docker Image: ${WORKSPACE}/${REPO}/${IMG}" + + +# 1) Get all associated tags from the repo for the image +getDockerTags () { + + # 1) Get all applicable tags for the image + echo + TAGS_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}/tags/list")" + + mapfile -t TAGS < <(jq -r ' + if type=="object" then + .tags[] + else + .. | objects | .tags? // empty + end + ' <<< "${TAGS_JSON}" | awk 'NF' | sort -u) + + if (( ${#TAGS[@]} == 0 )); then + echo "No tags found for the image." + exit 1 + fi + + nTAGS="${TAGS[@]}" + +} + +getDigestData () { + + local digest="$1" + mapfile -t ARCHS < <(jq -r --arg d "${digest}" ' + if type=="object" and (.manifests? // empty) then + .manifests[]? + | select(.digest == $d ) + | ((.platform.os // "") + "/" + (.platform.architecture // "")) + else + .. | objects | .architecture? // empty + end + ' <<< "${MANIFEST_JSON}" | awk 'NF' | sort -u) + + if (( ${#ARCHS[@]} == 0 )); then + echo "No architecture data found." + exit 1 + fi + + # Get the package data from Cloudsmith API packages list endpoint + getPackageData () { + + #echo "Fetching data for the images." + local digest="$1" + local version="${digest#*:}" # Strip sha256: from string + + # Get package data using the query string "version:" + PKG_DETAILS="$(curl -sS "${AUTH_HEADER[@]}" \ + -H "Cache-Control: no-cache" \ + --get "${API_BASE}?query=version:${version}")" + + mapfile -t STATUS < <(jq -r ' + .. | objects | .status_str + ' <<< "${PKG_DETAILS}" | awk 'NF' | sort -u) + + mapfile -t DOWNLOADS < <(jq -r ' + .. | objects | .downloads + ' <<< "${PKG_DETAILS}" | awk 'NF' | sort -u) + + + # 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 + + if (( ${#DOWNLOADS[@]} == 3 )); then + echo " |____ Downloads: ${DOWNLOADS[1]}" + count=${DOWNLOADS[1]} + totalDownloads=$((totalDownloads+count)) + else + echo " |____ Downloads: ${DOWNLOADS[0]}" + fi + + } + + echo " - ${digest}" + echo " - Platform: ${ARCHS}" + getPackageData "${digest}" + + } + + +# Get the individual digests for the tag +getDockerDigests () { + + local nTAG="$1" + 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}")" + + echo + echo "🐳 ${WORKSPACE}/${REPO}/${IMG}:${nTAG}" + echo " Index Digest: ${index_digest}" + + + 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/${nTAG}")" + + + # Parse out digest(s) and architectures + # - Prefer `.manifests[].digest` (typical manifest list) + # - Fallback to any `.digest` fields if needed, then unique + 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) + + if (( ${#DIGESTS[@]} == 0 )); then + echo "No digests found." + exit 1 + fi + + for i in "${!DIGESTS[@]}"; do + echo + getDigestData "${DIGESTS[i]}" + echo + done + echo " |___ Total Downloads: ${totalDownloads}" + +} + + +# 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 + +echo +for t in "${!images[@]}"; do + getDockerDigests "${images[t]}" +done + + + + + + + diff --git a/README.md b/README.md index eb6c7b9..c5fca33 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,6 @@ -# 🚀 Cloudsmith CENG Template +# 🚀 Cloudsmith Support Engineering -A reusable template repository maintained by the **Customer Engineering (CENG)** team at Cloudsmith. -This repo is intended to accelerate the development of examples, scripts, integrations, and demo workflows that help customers use Cloudsmith more effectively. - ---- - -## 📦 What’s Inside - -- GitHub Issue Forms for bugs and feature requests -- CI/CD example workflow (Python-based) -- Contribution and pull request templates -- Environment variable and code linting examples -- Directory structure for `src/` and `tests/` - ---- - -## 📁 Structure - -``` -. -├── .github/ # GitHub-specific automation and templates -│ ├── ISSUE_TEMPLATE/ # Issue forms using GitHub Issue Forms -│ │ ├── bug_report.yml # Form for reporting bugs -│ │ └── feature_request.yml # Form for suggesting features -│ ├── workflows/ # GitHub Actions workflows (e.g., CI pipelines) -│ ├── PULL_REQUEST_TEMPLATE.md # Template used when creating pull requests -│ └── CODEOWNERS # Defines reviewers for specific paths -├── src/ # Scripts, API integrations, or example tools -├── tests/ # Tests for scripts and tools in src/ -├── .env.example # Sample environment config (e.g., API keys) -├── .gitignore # Ignore rules for Git-tracked files -├── .editorconfig # Code style config to ensure consistency across IDEs -├── CHANGELOG.md # Log of project changes and version history -├── CONTRIBUTING.md # Guidelines and checklists for contributors -├── LICENSE # Licensing information (Apache 2.0) -└── README.md # This file -``` - ---- - -## 🛠 Getting Started - -1. Clone the template: - ```bash - git clone https://github.com/cloudsmith-examples/ceng-template.git - cd ceng-template - ``` - -2. Install any dependencies or activate your environment. - -3. Start building your example in the `src/` directory. - -4. Use the `.env.example` as a guide for credentials if needed. - ---- - -## 🧩 Use Cases - -- Building and testing Cloudsmith integrations for CI/CD platforms -- Creating reproducible customer issue examples -- Building Cloudsmith CLI or API automations -- Prototyping workflows for CI/CD platforms +A collection of useful resources for assisting with various components of Cloudsmith. ---