160 lines
4.9 KiB
Bash
Executable File
160 lines
4.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
SEARCH_DIR="$SCRIPT_DIR"
|
|
CONFIG_PATH=""
|
|
|
|
while true; do
|
|
CANDIDATE="$SEARCH_DIR/.clawdbot/credentials/portainer/config.json"
|
|
if [[ -f "$CANDIDATE" ]]; then
|
|
CONFIG_PATH="$CANDIDATE"
|
|
break
|
|
fi
|
|
PARENT="$(dirname "$SEARCH_DIR")"
|
|
if [[ "$PARENT" == "$SEARCH_DIR" ]]; then
|
|
break
|
|
fi
|
|
SEARCH_DIR="$PARENT"
|
|
done
|
|
|
|
if [[ -z "$CONFIG_PATH" && -f "$HOME/.clawdbot/credentials/portainer/config.json" ]]; then
|
|
CONFIG_PATH="$HOME/.clawdbot/credentials/portainer/config.json"
|
|
fi
|
|
|
|
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 "<stack-name>"
|
|
|
|
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:
|
|
workspace .clawdbot/credentials/portainer/config.json or ~/.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 <none>)
|
|
DANGLING="$(printf '%s' "$images_body" | jq -r '.[] | select(.RepoTags == null or (.RepoTags | length == 0) or (.RepoTags[0] | startswith("<none>"))) | .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"
|