|
| 1 | +#!/bin/bash |
| 2 | +# |
| 3 | +# Cleanup old ECR images while preserving prod, stage, and latest tags |
| 4 | +# |
| 5 | +# This script deletes tagged images older than a specified number of days, |
| 6 | +# but always preserves images tagged with 'prod', 'stage', or 'latest'. |
| 7 | +# |
| 8 | +# Usage: |
| 9 | +# ./scripts/cleanup-ecr.sh [days] [repository-name] |
| 10 | +# |
| 11 | +# Examples: |
| 12 | +# ./scripts/cleanup-ecr.sh 180 spellbot-app # Delete images older than 180 days |
| 13 | +# ./scripts/cleanup-ecr.sh 90 # Delete images older than 90 days (default repo) |
| 14 | +# |
| 15 | + |
| 16 | +set -euo pipefail |
| 17 | + |
| 18 | +# Configuration |
| 19 | +DEFAULT_DAYS=180 |
| 20 | +DEFAULT_REPOSITORY="spellbot-app" |
| 21 | +AWS_REGION="${AWS_REGION:-us-east-1}" |
| 22 | + |
| 23 | +# Parse arguments |
| 24 | +DAYS="${1:-$DEFAULT_DAYS}" |
| 25 | +REPOSITORY="${2:-$DEFAULT_REPOSITORY}" |
| 26 | + |
| 27 | +# Colors for output |
| 28 | +RED='\033[0;31m' |
| 29 | +GREEN='\033[0;32m' |
| 30 | +YELLOW='\033[1;33m' |
| 31 | +NC='\033[0m' # No Color |
| 32 | + |
| 33 | +# Helper functions |
| 34 | +log() { |
| 35 | + echo -e "${GREEN}[INFO]${NC} $*" |
| 36 | +} |
| 37 | + |
| 38 | +warn() { |
| 39 | + echo -e "${YELLOW}[WARN]${NC} $*" |
| 40 | +} |
| 41 | + |
| 42 | +error() { |
| 43 | + echo -e "${RED}[ERROR]${NC} $*" >&2 |
| 44 | +} |
| 45 | + |
| 46 | +success() { |
| 47 | + echo -e "${GREEN}[SUCCESS]${NC} $*" |
| 48 | +} |
| 49 | + |
| 50 | +# Validate inputs |
| 51 | +if ! [[ "$DAYS" =~ ^[0-9]+$ ]]; then |
| 52 | + error "Days must be a positive integer" |
| 53 | + exit 1 |
| 54 | +fi |
| 55 | + |
| 56 | +log "ECR Image Cleanup Configuration:" |
| 57 | +log " Repository: $REPOSITORY" |
| 58 | +log " Region: $AWS_REGION" |
| 59 | +log " Delete images older than: $DAYS days" |
| 60 | +log " Protected tags: prod, stage, latest" |
| 61 | +echo "" |
| 62 | + |
| 63 | +# Calculate cutoff timestamp (seconds since epoch) |
| 64 | +CUTOFF_TIMESTAMP=$(date -u -v-"${DAYS}"d +%s 2>/dev/null || date -u -d "$DAYS days ago" +%s) |
| 65 | +CUTOFF_DATE=$(date -u -r "$CUTOFF_TIMESTAMP" +%Y-%m-%d 2>/dev/null || date -u -d "@$CUTOFF_TIMESTAMP" +%Y-%m-%d) |
| 66 | + |
| 67 | +log "Cutoff date: $CUTOFF_DATE" |
| 68 | +echo "" |
| 69 | + |
| 70 | +# Check if repository exists |
| 71 | +if ! aws ecr describe-repositories \ |
| 72 | + --repository-names "$REPOSITORY" \ |
| 73 | + --region "$AWS_REGION" \ |
| 74 | + --output json > /dev/null 2>&1; then |
| 75 | + error "Repository '$REPOSITORY' not found in region '$AWS_REGION'" |
| 76 | + exit 1 |
| 77 | +fi |
| 78 | + |
| 79 | +# Get all images |
| 80 | +log "Fetching images from repository..." |
| 81 | +IMAGES=$(aws ecr describe-images \ |
| 82 | + --repository-name "$REPOSITORY" \ |
| 83 | + --region "$AWS_REGION" \ |
| 84 | + --output json) |
| 85 | + |
| 86 | +if [[ -z "$IMAGES" || "$IMAGES" == "null" ]]; then |
| 87 | + error "Failed to fetch images from repository" |
| 88 | + exit 1 |
| 89 | +fi |
| 90 | + |
| 91 | +# Find images to delete |
| 92 | +log "Analyzing images..." |
| 93 | +IMAGES_TO_DELETE=$(echo "$IMAGES" | jq -r --arg cutoff "$CUTOFF_TIMESTAMP" ' |
| 94 | + .imageDetails[] | |
| 95 | + select(.imageTags != null) | |
| 96 | + select( |
| 97 | + (.imageTags | any(. == "prod" or . == "stage" or . == "latest")) | not |
| 98 | + ) | |
| 99 | + select( |
| 100 | + (.imagePushedAt | fromdateiso8601) < ($cutoff | tonumber) |
| 101 | + ) | |
| 102 | + { |
| 103 | + digest: .imageDigest, |
| 104 | + tags: (.imageTags | join(", ")), |
| 105 | + pushedAt: (.imagePushedAt | todate) |
| 106 | + } |
| 107 | +') |
| 108 | + |
| 109 | +if [[ -z "$IMAGES_TO_DELETE" ]]; then |
| 110 | + success "No images found matching deletion criteria" |
| 111 | + exit 0 |
| 112 | +fi |
| 113 | + |
| 114 | +# Count images to delete |
| 115 | +IMAGE_COUNT=$(echo "$IMAGES_TO_DELETE" | jq -s 'length') |
| 116 | + |
| 117 | +warn "Found $IMAGE_COUNT image(s) to delete:" |
| 118 | +echo "" |
| 119 | +echo "$IMAGES_TO_DELETE" | jq -r '" Tags: \(.tags)\n Pushed: \(.pushedAt)\n Digest: \(.digest)\n"' |
| 120 | + |
| 121 | +# Confirm deletion |
| 122 | +read -p "Do you want to delete these images? (yes/no): " -r |
| 123 | +echo "" |
| 124 | +if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then |
| 125 | + log "Deletion cancelled" |
| 126 | + exit 0 |
| 127 | +fi |
| 128 | + |
| 129 | +# Delete images |
| 130 | +log "Deleting images..." |
| 131 | +DELETED_COUNT=0 |
| 132 | +FAILED_COUNT=0 |
| 133 | + |
| 134 | +while IFS= read -r digest; do |
| 135 | + if aws ecr batch-delete-image \ |
| 136 | + --repository-name "$REPOSITORY" \ |
| 137 | + --region "$AWS_REGION" \ |
| 138 | + --image-ids "imageDigest=$digest" \ |
| 139 | + --output json > /dev/null 2>&1; then |
| 140 | + ((DELETED_COUNT++)) |
| 141 | + log "Deleted image: $digest" |
| 142 | + else |
| 143 | + ((FAILED_COUNT++)) |
| 144 | + error "Failed to delete image: $digest" |
| 145 | + fi |
| 146 | +done < <(echo "$IMAGES_TO_DELETE" | jq -r '.digest') |
| 147 | + |
| 148 | +echo "" |
| 149 | +success "Cleanup complete!" |
| 150 | +log " Deleted: $DELETED_COUNT image(s)" |
| 151 | +if [[ $FAILED_COUNT -gt 0 ]]; then |
| 152 | + warn " Failed: $FAILED_COUNT image(s)" |
| 153 | +fi |
| 154 | + |
0 commit comments