-
Bug
-
Resolution: Unresolved
-
Critical
-
None
-
quay-v3.17.0
CRITICAL BUG: Auto-Prune Policy Fails When Immutable Tags Exist
Executive Summary
When an auto-prune policy is configured in a repository containing immutable tags, the auto-prune job will FAIL instead of gracefully skipping immutable tags. This leaves repositories in an inconsistent state and makes auto-prune unreliable for production use.
Severity: CRITICAL
Impact: Auto-prune feature is broken for any repository with immutable tags
Effort: 2-3 hours (simple fix: add 2 lines of code)
The Bug in Simple Terms
What SHOULD Happen
- Auto-prune policy runs (e.g., keep only 10 tags)
- Immutable tags are skipped gracefully
- Only mutable tags are deleted
- Job completes successfully
- Repository has correct number of remaining tags
What ACTUALLY Happens
- Auto-prune policy runs
- Selects tags for deletion WITHOUT filtering out immutable tags
- Attempts to delete immutable tag
- delete_tag() raises ImmutableTagException
- Entire auto-prune job FAILS
- Repository left in inconsistent state (some tags deleted, some remain)
- User sees confusing error message
Reproduction Steps
# 1. Create repository with 100 tags (5 immutable) # 2. Create auto-prune policy: Keep only 1 tag curl -X POST -d '{"method": "number_of_tags", "value": 1}' https://quay.io/api/v1/repository/org/repo/autoprunepolicy/ # 3. Wait for auto-prune worker to run # 4. Observe: Job FAILS when encountering immutable tag # 5. Repository left with ~50 tags (inconsistent state)
Expected Result
- Delete 99 mutable tags
- Keep 1 newest tag
- Job status: SUCCESS
- All immutable tags preserved
Actual Result
- Deletes some tags successfully
- Encounters immutable tag
- ImmutableTagException raised
- Job status: FAILED
- Repository has 40-60 tags remaining (inconsistent)
- Error: Error deleting tag with name: v1.0.0...
Root Cause Analysis
Bug Location 1: Tag Selection Missing Immutability Filter
File: data/model/oci/tag.py
Function: fetch_paginated_autoprune_repo_tags_by_number()
Lines: 909-914
query = Tag.select(Tag.id, Tag.name).where( Tag.repository_id == repo_id, Tag.lifetime_end_ms > now_ms, Tag.hidden == False, # MISSING: Tag.immutable == False )
Issue: Query does NOT filter out immutable tags from selection.
Bug Location 2: Creation Date Query Also Missing Filter
File: data/model/oci/tag.py
Function: fetch_paginated_autoprune_repo_tags_older_than_ms()
Lines: 955-960
Same problem - immutable tags included in deletion candidates.
Bug Location 3: Generic Exception Handling Causes Job Failure
File: data/model/autoprune.py
Function: prune_tags()
Lines: 587-603
def prune_tags(tags, repo, namespace): for tag in tags: try: tag = oci.tag.delete_tag(repo.id, tag.name) except Exception as err: # Catches ImmutableTagException raise Exception(...) # Re-raises, job FAILS
Issue: Generic exception handler catches ImmutableTagException and re-raises it, causing entire auto-prune job to fail instead of skipping the immutable tag.
Recommended Fix
Fix #1: Add Immutability Filter (PRIMARY FIX)
File: data/model/oci/tag.py
Line 913 in fetch_paginated_autoprune_repo_tags_by_number():
query = Tag.select(Tag.id, Tag.name).where( Tag.repository_id == repo_id, Tag.lifetime_end_ms > now_ms, Tag.hidden == False, Tag.immutable == False, # ADD THIS LINE )
Line 959 in fetch_paginated_autoprune_repo_tags_older_than_ms():
query = Tag.select(Tag.id, Tag.name).where( Tag.repository_id == repo_id, Tag.lifetime_end_ms > now_ms, Tag.hidden == False, Tag.immutable == False, # ADD THIS LINE )
Fix #2: Graceful Exception Handling (DEFENSE IN DEPTH)
File: data/model/autoprune.py
def prune_tags(tags, repo, namespace): for tag in tags: try: tag = oci.tag.delete_tag(repo.id, tag.name) if tag is not None: log.log_action(...) except ImmutableTagException as err: logger.info("Skipping immutable tag during auto-prune") continue # Skip to next tag instead of failing except Exception as err: raise Exception(...)
Impact Assessment
Severity: CRITICAL
Why Critical:
- Auto-prune feature is unreliable with immutable tags
- Jobs fail unexpectedly
- Repository state becomes inconsistent
- No workaround available
- Affects production deployments
- User experience is poor (confusing errors)
Affected Components
- Auto-prune worker
- All repositories using auto-prune policies
- Any repository with immutable tags
- Tag lifecycle management
Real-World Scenarios
Scenario 1: Production Image Registry
- 500 tags in production repo
- 50 critical release tags marked immutable
- Auto-prune policy: Keep last 100 tags
- Result: Auto-prune FAILS, repository has inconsistent number of tags
Scenario 2: Development Repository
- 200 tags from CI/CD builds
- 10 stable tags marked immutable
- Auto-prune policy: Delete tags older than 30 days
- Result: Auto-prune FAILS when encountering old immutable tag
Files Requiring Changes
| File | Lines | Change Required | Effort |
|---|---|---|---|
| data/model/oci/tag.py | 913 | Add Tag.immutable == False filter | 1 line |
| data/model/oci/tag.py | 959 | Add Tag.immutable == False filter | 1 line |
| data/model/autoprune.py | 584-603 | Add graceful exception handling | 10 lines |
| workers/test/test_autopruneworker.py | NEW | Add comprehensive test coverage | 50-100 lines |
Total Effort: 2-3 hours including tests
Related Issues
| Ticket | Issue | Status |
|---|---|---|
| PROJQUAY-10499 | Immutable tags can have expiration | Filed |
| PROJQUAY-10500 | Label errors show Undefined | Filed |
| PROJQUAY-10501 | Contradictory policies allowed | Filed |
| PROJQUAY-10502 | Bulk delete enabled for immutable tags | Filed |
| PROJQUAY-10504 | Manifest delete by digest bypasses immutability | Filed |
All recent immutability-related bugs show a pattern of incomplete immutability enforcement.
Summary
The Bug: Auto-prune tag selection queries do not filter out immutable tags, causing auto-prune jobs to fail when encountering immutable tags.
The Risk: Auto-prune feature is unreliable for production use with immutable tags. Repository state becomes inconsistent.
The Fix: Add Tag.immutable == False filter to two query locations (2 lines of code) plus graceful exception handling (10 lines).
The Effort: 2-3 hours including comprehensive tests.
The Priority: P0 - CRITICAL (core feature broken, simple fix, high user impact).
Reported by: Claude Code Automated Analysis
Date: 2026-02-05
Analysis Method: Comprehensive codebase review with execution flow tracing