Uploaded image for project: 'Project Quay'
  1. Project Quay
  2. PROJQUAY-10508

Quay 3.17 Auto-Prune Policy Fails When Immutable Tags Exist - Tag Selection Does Not Filter Immutability

XMLWordPrintable

    • Icon: Bug Bug
    • Resolution: Unresolved
    • Icon: Critical Critical
    • None
    • quay-v3.17.0
    • quay

      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

              rhn-support-bpratt Brady Pratt
              lzha1981 luffy zhang
              Votes:
              0 Vote for this issue
              Watchers:
              2 Start watching this issue

                Created:
                Updated: