#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CONFIG_PATH="/home/node/.openclaw/workspace/.clawdbot/credentials/portainer/config.json" err() { echo "Error: $*" >&2 exit 1 } require_cmd() { command -v "$1" >/dev/null 2>&1 || err "Required command not found: $1" } usage() { cat >&2 <<'EOF' Usage: prune-stack-images.sh "" Removes unused images that were previously associated with a stack. Run this after an update-stack --pull completes to clean up old image versions. Requires config at: /home/node/.openclaw/workspace/.clawdbot/credentials/portainer/config.json EOF exit 2 } [[ $# -eq 1 ]] || usage STACK_NAME="$1" require_cmd curl require_cmd jq [[ -f "$CONFIG_PATH" ]] || err "Missing config file: $CONFIG_PATH" BASE_URL="$(jq -r '.base_url // empty' "$CONFIG_PATH")" API_KEY="$(jq -r '.api_key // empty' "$CONFIG_PATH")" [[ -n "$BASE_URL" ]] || err "config.base_url is missing" [[ -n "$API_KEY" ]] || err "config.api_key is missing" BASE_URL="${BASE_URL%/}" # Resolve stack ID from name STACK_ID="$(bash "$SCRIPT_DIR/get-stack-id.sh" "$STACK_NAME")" [[ -n "$STACK_ID" ]] || err "Failed to resolve stack ID for: $STACK_NAME" # Get stack info for EndpointId STACK_INFO="$(curl -sS -w $'\n%{http_code}' \ -H "X-API-Key: $API_KEY" \ "$BASE_URL/api/stacks/$STACK_ID")" http_code="$(printf '%s' "$STACK_INFO" | tail -n1)" body="$(printf '%s' "$STACK_INFO" | sed '$d')" if [[ "$http_code" -lt 200 || "$http_code" -ge 300 ]]; then msg="$(printf '%s' "$body" | jq -r '.message // empty' 2>/dev/null || true)" [[ -n "$msg" ]] || msg="$body" err "Failed to fetch stack info (HTTP $http_code): $msg" fi ENDPOINT_ID="$(printf '%s' "$body" | jq -r '.EndpointId')" [[ -n "$ENDPOINT_ID" && "$ENDPOINT_ID" != "null" ]] || err "Could not determine endpoint ID for stack" # Get images currently in use by this stack's containers FILTERS="$(jq -n --arg project "$STACK_NAME" '{"label": ["com.docker.compose.project=\($project)"]}')" CONTAINERS_RESPONSE="$(curl -sS -w $'\n%{http_code}' \ -H "X-API-Key: $API_KEY" \ -G \ --data-urlencode "all=1" \ --data-urlencode "filters=$FILTERS" \ "$BASE_URL/api/endpoints/$ENDPOINT_ID/docker/containers/json")" containers_http_code="$(printf '%s' "$CONTAINERS_RESPONSE" | tail -n1)" containers_body="$(printf '%s' "$CONTAINERS_RESPONSE" | sed '$d')" if [[ "$containers_http_code" -lt 200 || "$containers_http_code" -ge 300 ]]; then err "Failed to fetch containers (HTTP $containers_http_code)" fi # Extract image names/repoTags used by current containers CURRENT_IMAGES="$(printf '%s' "$containers_body" | jq -r '.[].Image' | sort -u)" if [[ -z "$CURRENT_IMAGES" ]]; then echo "No containers found for stack '$STACK_NAME'" exit 0 fi echo "Current images in use by stack '$STACK_NAME':" echo "$CURRENT_IMAGES" | while read -r img; do echo " - $img"; done # Get all images on the endpoint IMAGES_RESPONSE="$(curl -sS -w $'\n%{http_code}' \ -H "X-API-Key: $API_KEY" \ "$BASE_URL/api/endpoints/$ENDPOINT_ID/docker/images/json?all=false")" images_http_code="$(printf '%s' "$IMAGES_RESPONSE" | tail -n1)" images_body="$(printf '%s' "$IMAGES_RESPONSE" | sed '$d')" if [[ "$images_http_code" -lt 200 || "$images_http_code" -ge 300 ]]; then err "Failed to fetch images (HTTP $images_http_code)" fi # Find dangling images (no RepoTags or only ) DANGLING="$(printf '%s' "$images_body" | jq -r '.[] | select(.RepoTags == null or (.RepoTags | length == 0) or (.RepoTags[0] | startswith(""))) | .Id')" if [[ -z "$DANGLING" ]]; then echo "No dangling images found" exit 0 fi echo "" echo "Dangling images found:" echo "$DANGLING" | while read -r img; do echo " - $img"; done # Remove dangling images echo "" echo "Removing dangling images..." for IMAGE_ID in $DANGLING; do DELETE_RESPONSE="$(curl -sS -w $'\n%{http_code}' -X DELETE \ -H "X-API-Key: $API_KEY" \ "$BASE_URL/api/endpoints/$ENDPOINT_ID/docker/images/$IMAGE_ID?force=false")" delete_http_code="$(printf '%s' "$DELETE_RESPONSE" | tail -n1)" delete_body="$(printf '%s' "$DELETE_RESPONSE" | sed '$d')" if [[ "$delete_http_code" -ge 200 && "$delete_http_code" -lt 300 ]]; then echo "Removed: $IMAGE_ID" else msg="$(printf '%s' "$delete_body" | jq -r '.message // empty' 2>/dev/null || true)" [[ -n "$msg" ]] || msg="$delete_body" echo "Warning: Could not remove $IMAGE_ID: $msg" fi done echo "" echo "Pruning complete"