# Forgejo OCI Registry Cleanup CronJob # Deletes old image tags, keeping only the latest N versions per repository. # Useful for retiring old builds when new versions are pushed. --- apiVersion: v1 kind: ConfigMap metadata: name: forgejo-registry-cleanup-script namespace: cicd data: cleanup.sh: | #!/bin/bash set -eo pipefail # Configuration REGISTRY_HOST="${REGISTRY_HOST:-forgejo.riotpiao.com}" REGISTRY_URL="https://${REGISTRY_HOST}" KEEP_VERSIONS="${KEEP_VERSIONS:-3}" # Keep latest N versions per image DRY_RUN="${DRY_RUN:-false}" # Load credentials from mounted secret REGISTRY_USER="${REGISTRY_USER:-_json_key}" REGISTRY_PASS="$(cat /etc/registry-secret/password 2>/dev/null || echo '')" log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" } error() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2 return 1 } # Verify crane is available if ! command -v crane &> /dev/null; then error "crane not found. Install google/crane image for registry operations." exit 1 fi log "Starting Forgejo registry cleanup" log "Registry: $REGISTRY_URL" log "Keep versions: $KEEP_VERSIONS per image" log "Dry run: $DRY_RUN" # Authenticate crane with registry if [ -n "$REGISTRY_PASS" ]; then echo "$REGISTRY_PASS" | crane auth login "$REGISTRY_HOST" -u "$REGISTRY_USER" --password-stdin log "Authenticated to $REGISTRY_HOST" fi # List all repositories (catalog) # Note: This endpoint requires the registry to expose /v2/_catalog (standard OCI) # If not available, images must be discovered another way CATALOG=$(curl -s -u "${REGISTRY_USER}:${REGISTRY_PASS}" \ "${REGISTRY_URL}/v2/_catalog" | grep -o '"repositories":\[\K[^]]*' || echo '') if [ -z "$CATALOG" ]; then log "WARNING: Could not retrieve catalog from ${REGISTRY_URL}/v2/_catalog" log "Registry may not expose _catalog endpoint or credentials invalid" exit 0 fi # Parse repositories from catalog JSON REPOS=$(echo "$CATALOG" | grep -o '"[^"]*"' | tr -d '"') TOTAL_DELETED=0 for REPO in $REPOS; do log "Processing repository: $REPO" IMAGE="${REGISTRY_HOST}/${REPO}" # Get all tags for this image TAGS=$(crane ls "$IMAGE" 2>/dev/null || echo "") if [ -z "$TAGS" ]; then log " No tags found for $REPO (or access denied)" continue fi # Filter out 'latest' tag and sort by creation time (newer first) # Note: crane doesn't provide direct date sorting; we use the order returned # Assumption: tags are returned newest first (not always true) TAG_COUNT=$(echo "$TAGS" | wc -l) if [ "$TAG_COUNT" -le "$KEEP_VERSIONS" ]; then log " $REPO: $TAG_COUNT tags total, keeping all (≤ $KEEP_VERSIONS)" continue fi # Get tags to delete (all except the first N) TAGS_TO_DELETE=$(echo "$TAGS" | tail -n +$((KEEP_VERSIONS + 1))) for TAG in $TAGS_TO_DELETE; do FULL_IMAGE="${IMAGE}:${TAG}" DELETED_SIZE="0" if [ "$DRY_RUN" = "true" ]; then log " [DRY RUN] Would delete: $FULL_IMAGE" else if crane delete "$FULL_IMAGE" 2>&1; then log " Deleted: $FULL_IMAGE" ((TOTAL_DELETED++)) else error "Failed to delete $FULL_IMAGE (may already be deleted)" fi fi done done log "Cleanup complete. Total images deleted: $TOTAL_DELETED" --- apiVersion: batch/v1 kind: CronJob metadata: name: forgejo-registry-cleanup namespace: cicd labels: app: forgejo-registry-cleanup spec: # Run at 2 AM UTC every day (adjust as needed) schedule: "0 2 * * *" # Keep last 3 successful/failed runs for debugging successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 3 # Suspend if needed (set to false to enable) suspend: false jobTemplate: spec: # Cleanup jobs after 6 hours whether they succeeded or failed ttlSecondsAfterFinished: 21600 template: metadata: labels: app: forgejo-registry-cleanup spec: serviceAccountName: forgejo-registry-cleanup restartPolicy: OnFailure containers: - name: cleanup # Use google/crane for registry operations image: gcr.io/go-containerregistry/crane:latest imagePullPolicy: IfNotPresent env: - name: REGISTRY_HOST value: "forgejo.riotpiao.com" - name: KEEP_VERSIONS value: "3" # Keep 3 latest versions - name: DRY_RUN value: "false" # Set to "true" for dry-run mode - name: REGISTRY_USER valueFrom: secretKeyRef: name: forgejo-registry-token key: username optional: true volumeMounts: - name: script mountPath: /scripts - name: registry-secret mountPath: /etc/registry-secret readOnly: true # Run cleanup script via entrypoint override command: - /bin/sh - -c - | # Install bash and curl if needed apk add --no-cache bash curl chmod +x /scripts/cleanup.sh /scripts/cleanup.sh resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 512Mi # Safety: kill after 30 min (prevents hanging on large registries) securityContext: runAsNonRoot: true runAsUser: 65534 allowPrivilegeEscalation: false readOnlyRootFilesystem: false capabilities: drop: - ALL volumes: - name: script configMap: name: forgejo-registry-cleanup-script defaultMode: 0755 - name: registry-secret secret: secretName: forgejo-registry-token optional: true --- apiVersion: v1 kind: ServiceAccount metadata: name: forgejo-registry-cleanup namespace: cicd --- # No RBAC needed: this pod only talks to the registry API (external service) # If expanded to manage in-cluster resources, add Role/RoleBinding here