diff --git a/CHANGELOG.md b/CHANGELOG.md index f2e8172..6dfe78c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,21 +6,9 @@ This project follows [Semantic Versioning](https://semver.org). ## [Unreleased] -### Added -- Initial structure for Customer Engineering template repo -- GitHub Issue Forms for bug reports and feature requests -- CI workflow with Python setup - -### Changed -- N/A - -### Fixed -- N/A - ---- - -## [1.0.0] - 2025-07-11 +## [Cloudsmith Docker Sleuth] [v1.0] [2025-12-12] ### Added -- πŸŽ‰ First release of the CENG template! -- Includes CI, GitHub forms, CONTRIBUTING, CODEOWNERS, and PR template \ No newline at end of file +- Analyze your Cloudsmith Docker repositories with a hierarchical view of multi-arch tags, +manifest digests, platform support, sync status, and download statistics. +- Query untagged (orphaned) multi-arch images and delete them. diff --git a/Docker/Cloudsmith Docker Sleuth/README.md b/Docker/Cloudsmith Docker Sleuth/README.md new file mode 100644 index 0000000..3899a85 --- /dev/null +++ b/Docker/Cloudsmith Docker Sleuth/README.md @@ -0,0 +1,114 @@ +# Cloudsmith Docker Sleuth + +
+
+β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•—      β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•—   β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ•—   β–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•—  β–ˆβ–ˆβ•—
+β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β•šβ•β•β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘
+β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β–ˆβ–ˆβ–ˆβ–ˆβ•”β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘
+β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘β•šβ•β•β•β•β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•‘
+β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β•šβ•β• β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘
+ β•šβ•β•β•β•β•β•β•šβ•β•β•β•β•β•β• β•šβ•β•β•β•β•β•  β•šβ•β•β•β•β•β• β•šβ•β•β•β•β•β• β•šβ•β•β•β•β•β•β•β•šβ•β•     β•šβ•β•β•šβ•β•   β•šβ•β•   β•šβ•β•  β•šβ•β•
+
+β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•—  β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—     β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•—     β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•—   β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•—  β–ˆβ–ˆβ•—
+β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—    β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β•šβ•β•β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘
+β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β• β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•    β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—  β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘
+β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•— β–ˆβ–ˆβ•”β•β•β•  β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—    β•šβ•β•β•β•β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ•”β•β•β•  β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•‘
+β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘    β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•   β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘
+β•šβ•β•β•β•β•β•  β•šβ•β•β•β•β•β•  β•šβ•β•β•β•β•β•β•šβ•β•  β•šβ•β•β•šβ•β•β•β•β•β•β•β•šβ•β•  β•šβ•β•    β•šβ•β•β•β•β•β•β•β•šβ•β•β•β•β•β•β•β•šβ•β•β•β•β•β•β• β•šβ•β•β•β•β•β•    β•šβ•β•   β•šβ•β•  β•šβ•β•
+
+ + +**Analyze your Cloudsmith Docker repositories with a hierarchical view of multi-arch tags, +manifest digests, platform support, sync status, and download statistics.** + +
+ + + +This Python script audits multi-architecture Docker images stored in your Cloudsmith repository. It interacts with both the Cloudsmith API and Docker Manifest V2 endpoints to provide a detailed analysis of your container images. + +Here is a summary of its capabilities: + +1. **Visualization & Hierarchy** + - **Rich Tables:** Uses the `rich` library to render formatted, colored terminal tables. + - **Manifest Lists:** Visually groups architecture-specific images (children) under their parent Manifest List tag. + - **Details:** Displays the Tag, Type (manifest/list vs. image), Platform (e.g., linux/amd64, linux/arm64), Status (with icons), Download Counts, and SHA256 Digests. +2. **Inspection Modes** + - **Single Image:** Can inspect a specific image repository (e.g., `my-org/my-repo/my-image`). + - **Full Catalog:** If no image name is provided, it automatically fetches the catalog and scans every image in the repository. + - **Detailed View:** The `--detailed` flag expands the output to show every individual child digest and its specific download count, rather than just the summary. +3. **Maintenance (Untagged Images)** + - **Detection:** The `--untagged` flag scans for "orphaned" manifest lists that exist but have no version tags associated with them. + - **Cleanup:** The `--untagged-delete` flag allows you to programmatically delete these untagged manifest lists to clean up the repository. +4. **Data Aggregation** + - It combines data from two sources: + - **Docker Manifests:** To determine architecture/OS platforms and digest relationships. + - **Cloudsmith API:** To retrieve processing status (Synced, Failed, In Progress) and download statistics. + +#### Query repository for images + + +#### Query a specific image and view detailed results. + + + +#### Query untagged images + + + +## Prerequisites + +1. **Python Environment** + Ensure you have Python 3 installed and the required library: + ```bash + pip install rich + ``` + +2. **Cloudsmith API Key** + Configure the Cloudsmith environment variable with your PAT or Service Account Token. + ```bash + export CLOUDSMITH_API_KEY= + ``` + +## How to use + +1. **Basic Usage** + Run the script targeting your Organization and Repository. + + - Scan a specific image: + ```bash + python3 multiarch.py my-org my-repo my-image + ``` + + - Scan ALL images in the repository: + (Omit the image name) + +2. **Advanced Flags** + | Flag | Description | + |-----------------------|--------------------------------------------------------------| + | `--detailed` | Shows every child digest (arch/os) and individual download counts. | + | `--untagged` | Finds manifest lists that have no tags (orphaned). | + | `--untagged-delete` | Deletes any untagged manifest lists found. | + +3. **Examples** + - Get a summary of all tags for my-image: + ```bash + python3 multiarch.py my-org my-repo my-image + ``` + + - See full breakdown (platforms & digests) for all images: + ```bash + python3 multiarch.py my-org my-repo --detailed + ``` + + - Delete untagged manifest lists: + ```bash + python3 multiarch.py my-org my-repo my-image --untagged-delete + ``` + + + + + + + diff --git a/Docker/Cloudsmith Docker Sleuth/example-detailed.gif b/Docker/Cloudsmith Docker Sleuth/example-detailed.gif new file mode 100644 index 0000000..ebfb21e Binary files /dev/null and b/Docker/Cloudsmith Docker Sleuth/example-detailed.gif differ diff --git a/Docker/Cloudsmith Docker Sleuth/example-untagged.gif b/Docker/Cloudsmith Docker Sleuth/example-untagged.gif new file mode 100644 index 0000000..b971fab Binary files /dev/null and b/Docker/Cloudsmith Docker Sleuth/example-untagged.gif differ diff --git a/Docker/Cloudsmith Docker Sleuth/example.gif b/Docker/Cloudsmith Docker Sleuth/example.gif new file mode 100644 index 0000000..14d8fbe Binary files /dev/null and b/Docker/Cloudsmith Docker Sleuth/example.gif differ diff --git a/Docker/Cloudsmith Docker Sleuth/multiarch.py b/Docker/Cloudsmith Docker Sleuth/multiarch.py new file mode 100755 index 0000000..cabecd3 --- /dev/null +++ b/Docker/Cloudsmith Docker Sleuth/multiarch.py @@ -0,0 +1,418 @@ +#!/usr/bin/env python3 + +import sys +import os +import json +import argparse +import urllib.request +import urllib.error +from urllib.parse import urlencode +import concurrent.futures + +# Try to import rich +try: + from rich.console import Console + from rich.table import Table + from rich import box + from rich.text import Text +except ImportError: + print("Error: This script requires the 'rich' library.") + print("Please install it using: pip install rich") + sys.exit(1) + +# --- Configuration & Constants --- + +console = Console() + +# API Config +CLOUDSMITH_URL = os.environ.get("CLOUDSMITH_URL", "https://docker.cloudsmith.io") +API_KEY = os.environ.get("CLOUDSMITH_API_KEY") +AUTH_HEADER = {"Authorization": f"Bearer {API_KEY}"} if API_KEY else {} + +# --- Helper Functions --- + +def make_request(url, headers=None, method='GET', data=None): + """Performs an HTTP request and returns parsed JSON.""" + if headers is None: + headers = {} + + final_headers = {**AUTH_HEADER, **headers} + + req = urllib.request.Request(url, headers=final_headers, method=method) + if data: + req.data = data.encode('utf-8') + + try: + with urllib.request.urlopen(req) as response: + return json.loads(response.read().decode('utf-8')) + except urllib.error.HTTPError as e: + return None + except Exception as e: + # Avoid printing to stderr in threads to prevent garbled output + return None + +def find_key_recursive(obj, key): + """Recursively searches for a key in a dictionary/list and returns a list of values.""" + results = [] + if isinstance(obj, dict): + for k, v in obj.items(): + if k == key: + results.append(v) + elif isinstance(v, (dict, list)): + results.extend(find_key_recursive(v, key)) + elif isinstance(obj, list): + for item in obj: + results.extend(find_key_recursive(item, key)) + return results + +def format_status(status_str): + """Returns a rich-formatted status string.""" + if status_str == "Completed": return f"[green]{status_str}[/green] βœ…" + if status_str == "In Progress": return f"[yellow]{status_str}[/yellow] ⏳" + if status_str == "Quarantined": return f"[red]{status_str}[/red] ☠️" + if status_str == "Failed": return f"[bold red]{status_str}[/bold red] ❌" + return status_str + +# --- Core Logic --- + +def get_digest_data(workspace, repo, img, digest, ntag_display, platform="unknown"): + """Fetches data for a specific digest (child image) and returns row data.""" + + # 1. Fetch Manifest to get Architecture (Only if unknown) + if platform == "unknown": + manifest_url = f"{CLOUDSMITH_URL}/v2/{workspace}/{repo}/{img}/manifests/{digest}" + manifest_json = make_request(manifest_url, {"Accept": "application/vnd.oci.image.manifest.v1+json", "Cache-Control": "no-cache"}) + + if manifest_json: + if 'manifests' in manifest_json: + found = False + for m in manifest_json['manifests']: + if m.get('digest') == digest: + p = m.get('platform', {}) + platform = f"{p.get('os', '')}/{p.get('architecture', '')}" + found = True + break + if not found: + archs = find_key_recursive(manifest_json, 'architecture') + if archs: platform = archs[0] + else: + archs = find_key_recursive(manifest_json, 'architecture') + if archs: platform = archs[0] + + # 2. Get Package Data from API + version = digest.replace("sha256:", "") + api_url = f"https://api.cloudsmith.io/v1/packages/{workspace}/{repo}/?query=version:{version}" + pkg_details = make_request(api_url, {"Cache-Control": "no-cache"}) + + status_display = "" + dl = 0 + + if pkg_details: + statuses = set(find_key_recursive(pkg_details, 'status_str')) + status_parts = [format_status(s) for s in statuses] + status_display = " ".join(status_parts) + + downloads = find_key_recursive(pkg_details, 'downloads') + if len(downloads) >= 2: + dl = downloads[0] + elif len(downloads) > 0: + dl = downloads[0] + + # Return tuple of (Row Columns List, Download Count) + row_data = [ + f" └─ {ntag_display}", + "image", + platform, + status_display, + str(dl), + f"[dim]{digest}[/dim]" + ] + return row_data, dl + +def fetch_tag_data(workspace, repo, img, ntag, detailed=False): + """Fetches the manifest list for a tag and returns rows for the table.""" + + manifest_url = f"{CLOUDSMITH_URL}/v2/{workspace}/{repo}/{img}/manifests/{ntag}" + manifest_json = make_request(manifest_url, {"Accept": "application/vnd.oci.image.manifest.v1+json", "Cache-Control": "no-cache"}) + + if not manifest_json: + return [] + + # Parse out digests and platforms + children = [] + if 'manifests' in manifest_json: + for m in manifest_json['manifests']: + d = m.get('digest') + p = m.get('platform', {}) + os_name = p.get('os', 'linux') + arch = p.get('architecture', 'unknown') + plat = f"{os_name}/{arch}" + + if d and arch.lower() != 'unknown': + children.append({'digest': d, 'platform': plat}) + else: + # Fallback + digests = list(set(find_key_recursive(manifest_json, 'digest'))) + for d in digests: + children.append({'digest': d, 'platform': 'unknown'}) + + if not children: + return [] + + # Process children + children_rows = [] + total_downloads = 0 + + for child in children: + row, dl = get_digest_data(workspace, repo, img, child['digest'], ntag, platform=child['platform']) + children_rows.append(row) + total_downloads += dl + + # Fetch parent package info + api_url = f"https://api.cloudsmith.io/v1/packages/{workspace}/{repo}/?query=version:{ntag}" + pkg_details = make_request(api_url, {"Cache-Control": "no-cache"}) + + parent_status = "Unknown" + index_digest = "" + + if pkg_details and len(pkg_details) > 0: + parent_status = pkg_details[0].get('status_str', 'Unknown') + ver = pkg_details[0].get('version', '') + if ver and not ver.startswith('sha256:'): + index_digest = f"sha256:{ver}" + else: + index_digest = ver + + status_display = format_status(parent_status) + + rows = [] + # Parent Row + rows.append([ + f"[bold cyan]{ntag}[/bold cyan]", + "[magenta]manifest/list[/magenta]", + "multi", + status_display, + str(total_downloads), + f"[dim]{index_digest}[/dim]" + ]) + + # Children Rows + if detailed: + rows.extend(children_rows) + rows.append("SECTION") + + return rows + +def fetch_untagged_data(pkg, workspace, repo, img, detailed=False): + digest = pkg.get('version') + if digest and not digest.startswith('sha256:'): + digest = f"sha256:{digest}" + + status = pkg.get('status_str') + downloads = pkg.get('downloads', 0) + slug = pkg.get('slug') + + # Fetch manifest to get platforms + manifest_url = f"{CLOUDSMITH_URL}/v2/{workspace}/{repo}/{img}/manifests/{digest}" + manifest_json = make_request(manifest_url, {"Accept": "application/vnd.oci.image.manifest.v1+json", "Cache-Control": "no-cache"}) + + child_digests = [] + platform_str = "unknown" + + if manifest_json: + archs = set() + if 'manifests' in manifest_json: + for m in manifest_json['manifests']: + p = m.get('platform', {}) + os_name = p.get('os', 'linux') + arch = p.get('architecture', 'unknown') + plat = f"{os_name}/{arch}" + archs.add(plat) + + if arch.lower() != 'unknown': + child_digests.append({'digest': m['digest'], 'platform': plat}) + else: + archs.add("unknown") + + platform_str = " ".join(sorted(list(archs))) + + status_display = format_status(status) + + rows = [] + rows.append([ + "(untagged)", + "manifest/list", + platform_str, + status_display, + str(downloads), + digest + ]) + + if detailed: + for child in child_digests: + row, _ = get_digest_data(workspace, repo, img, child['digest'], "(untagged)", platform=child['platform']) + rows.append(row) + rows.append("SECTION") + + return rows, slug + +def get_untagged_images(workspace, repo, img, delete=False, detailed=False): + console.print("[bold]Searching for untagged manifest lists...[/bold]") + api_url = f"https://api.cloudsmith.io/v1/packages/{workspace}/{repo}/" + query = urlencode({'query': f"name:{img}"}) + full_url = f"{api_url}?{query}" + + packages = make_request(full_url, {"Cache-Control": "no-cache"}) + + untagged_pkgs = [] + if packages: + for p in packages: + if p.get('type_display') == 'manifest/list': + tags = p.get('tags', {}) + if not tags.get('version'): + untagged_pkgs.append(p) + + if not untagged_pkgs: + console.print("[yellow]No untagged manifest lists found.[/yellow]") + return + + # Create Table + table = Table(title="Untagged Manifest Lists", box=box.ROUNDED) + table.add_column("Tag", style="cyan") + table.add_column("Type", style="magenta") + table.add_column("Platform") + table.add_column("Status") + table.add_column("Downloads", justify="right") + table.add_column("Digest", style="dim") + + packages_to_delete = [] + + with console.status("[bold green]Fetching untagged data...[/bold green]"): + with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: + # Submit all tasks + futures = {executor.submit(fetch_untagged_data, pkg, workspace, repo, img, detailed): i for i, pkg in enumerate(untagged_pkgs)} + + results = {} + for future in concurrent.futures.as_completed(futures): + index = futures[future] + try: + results[index] = future.result() + except Exception as e: + console.print(f"[red]Error processing untagged image: {e}[/red]") + + # Add to table in original order + for i in range(len(untagged_pkgs)): + if i in results: + rows, slug = results[i] + packages_to_delete.append(slug) + for row in rows: + if row == "SECTION": + table.add_section() + else: + table.add_row(*row) + + console.print(table) + + if delete: + console.print("\n[bold red]Deleting untagged packages...[/bold red]") + for slug in packages_to_delete: + console.print(f" Deleting package: {slug}...", end=" ") + del_url = f"https://api.cloudsmith.io/v1/packages/{workspace}/{repo}/{slug}/" + req = urllib.request.Request(del_url, headers=AUTH_HEADER, method='DELETE') + try: + with urllib.request.urlopen(req): + console.print("[green]Deleted.[/green]") + except Exception as e: + console.print(f"[red]Failed: {e}[/red]") + +def main(): + parser = argparse.ArgumentParser(description="Docker Multi-Arch Inspector") + parser.add_argument("org", help="Cloudsmith Organization/User") + parser.add_argument("repo", help="Cloudsmith Repository") + parser.add_argument("img", nargs="?", help="Image Name (Optional - if omitted, scans all images)") + parser.add_argument("--untagged", action="store_true", help="Find untagged manifest lists") + parser.add_argument("--untagged-delete", action="store_true", help="Delete untagged manifest lists") + parser.add_argument("--detailed", action="store_true", help="Show detailed breakdown of digests") + + args = parser.parse_args() + + images_to_scan = [] + + if args.img: + images_to_scan.append(args.img) + else: + console.print(f"[bold]Fetching catalog for {args.org}/{args.repo}...[/bold]") + catalog_url = f"{CLOUDSMITH_URL}/v2/{args.org}/{args.repo}/_catalog" + catalog_json = make_request(catalog_url, {"Accept": "application/json", "Cache-Control": "no-cache"}) + + if catalog_json and 'repositories' in catalog_json: + images_to_scan = catalog_json['repositories'] + else: + console.print("[red]Failed to fetch catalog or no images found.[/red]") + sys.exit(1) + + for img_name in images_to_scan: + console.print(f"\nDocker Image: [bold blue]{args.org}/{args.repo}/{img_name}[/bold blue]") + + if args.untagged or args.untagged_delete: + get_untagged_images(args.org, args.repo, img_name, delete=args.untagged_delete, detailed=args.detailed) + else: + # Get Tags + tags_url = f"{CLOUDSMITH_URL}/v2/{args.org}/{args.repo}/{img_name}/tags/list" + tags_json = make_request(tags_url, {"Accept": "application/vnd.oci.image.manifest.v1+json", "Cache-Control": "no-cache"}) + + tags = [] + if tags_json: + raw_tags = find_key_recursive(tags_json, 'tags') + flat_tags = [] + for item in raw_tags: + if isinstance(item, list): + flat_tags.extend(item) + else: + flat_tags.append(item) + + tags = sorted(list(set(flat_tags))) + + if not tags: + console.print(f"[yellow]No tags found for {img_name}.[/yellow]") + continue + + console.print(f"Found matching tags: [bold]{len(tags)}[/bold]") + + # Create Main Table + table = Table(title=f"Image Analysis: {img_name}", box=box.ROUNDED) + table.add_column("Tag", style="cyan") + table.add_column("Type", style="magenta") + table.add_column("Platform") + table.add_column("Status") + table.add_column("Downloads", justify="right") + table.add_column("Digest", style="dim") + + with console.status(f"[bold green]Fetching data for {img_name}...[/bold green]"): + with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: + # Submit all tasks + future_to_tag = {executor.submit(fetch_tag_data, args.org, args.repo, img_name, t, args.detailed): t for t in tags} + + results = {} + for future in concurrent.futures.as_completed(future_to_tag): + tag = future_to_tag[future] + try: + results[tag] = future.result() + except Exception as exc: + console.print(f"[red]Tag {tag} generated an exception: {exc}[/red]") + + # Add to table in sorted order + for t in tags: + if t in results: + rows = results[t] + for row in rows: + if row == "SECTION": + table.add_section() + else: + table.add_row(*row) + + # Print the final table + console.print(table) + +if __name__ == "__main__": + main() \ No newline at end of file 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. ---