Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a4ed53b
fix: key index.db artifact/ImageStream identity on image content digest
yashvardhannanavati Sep 2, 2026
fffa76e
feat: add remote_branch_exists and get_index_tag helpers
yashvardhannanavati Aug 30, 2026
42ce2d9
feat: unprivileged extraction of configs+index.db from index image
yashvardhannanavati Aug 30, 2026
b0db932
feat: prepare_build_sources orchestrates normal vs divergent build paths
yashvardhannanavati Aug 30, 2026
de380a4
feat: route add requests through normal/divergent build sources
yashvardhannanavati Aug 30, 2026
31d072a
feat: route rm requests through normal/divergent build sources
yashvardhannanavati Aug 30, 2026
abcb41a
feat: route fbc-operations through normal/divergent build sources
yashvardhannanavati Aug 30, 2026
d60faf9
test: prove divergent never-merge guard with overwrite=True
yashvardhannanavati Aug 30, 2026
66d231b
docs: document branch=tag convention and divergent-tag builds
yashvardhannanavati Aug 30, 2026
8861631
fix: address code review findings on divergent-tag build path
yashvardhannanavati Sep 2, 2026
0da753d
feat: key index.db write on output image digest with warm-push
yashvardhannanavati Sep 2, 2026
4f4df4c
refactor: drop index.db rollback capture (content keys are immutable)
yashvardhannanavati Sep 2, 2026
961cd7e
feat: route handlers' index.db push through the output image digest
yashvardhannanavati Sep 2, 2026
ee6aa59
feat: bootstrap index.db from image on digest cache miss (read-through)
yashvardhannanavati Sep 2, 2026
b73a69f
docs: describe content-digest index.db cache naming
yashvardhannanavati Sep 2, 2026
b3210b7
fix: require hidden index.db, fail instead of falling back
yashvardhannanavati Sep 4, 2026
5124112
fix: extract image files via 'oc image extract', not tarfile
yashvardhannanavati Sep 4, 2026
bde9a0c
fix: treat declared-but-empty /configs as an empty catalog
yashvardhannanavati Sep 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,4 @@ Key modules:
- **Never edit existing Alembic migrations** — generate a new revision instead.
- **API ↔ Worker task signatures must match** — renaming args silently breaks in-flight requests.
- **Avoid making changes to the old worker architecture** - Always ask before making changes to the following functions - `handle_add_request`, `handle_rm_request`, `handle_merge_request`, `handle_create_empty_index_request`, `handle_fbc_operation_request`, `handle_regenerate_bundle_request`
- **Divergent-tag requests never merge their MR** — they reuse the base OCP branch's Konflux Component and must stay throw-away; the guard is `overwrite_from_index and not sources.is_divergent`. Never source their index.db from ORAS.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] scope-creep

The PR adds a project-level invariant rule to AGENTS.md that encodes the design decisions of the feature being introduced in this same PR. While documenting a feature alongside its implementation is reasonable, the rule should be reviewed independently as it establishes architectural precedent.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[high] protected-path

AGENTS.md is a protected governance file. This PR modifies it to document the divergent-tag invariant but has no linked issue authorizing changes to protected paths. Human approval is always required for protected-path changes regardless of context.

Suggested fix: Link an issue authorizing the AGENTS.md change, or obtain explicit human approval for the protected-path modification.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[high] protected-path

PR modifies a protected governance file (AGENTS.md) without a linked issue providing authorization. Human approval is required for all protected-path changes regardless of context.

33 changes: 33 additions & 0 deletions docker/containerized/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,39 @@ The worker configuration is in `docker/containerized/worker_config.py`. This fil
- Includes the containerized task modules
- Validates required configuration on startup

## Git Branch and Tag Semantics

The Git branch used for a request's catalog is keyed on the **image tag**, not a fixed OCP-version mapping. For the existing fleet this is a no-op, since prod index tags already equal the OCP version (e.g. `v4.19` → branch `v4.19`).

### Onboarding a Non-OCP Tag

To onboard a tag that isn't an OCP version (e.g. a custom or pre-release tag), a maintainer must:

1. Create a Git branch in the catalog repository named exactly after the tag.
2. Provision a Konflux Component for that branch, so pushes to it trigger a PipelineRun.

Once both exist, requests against that tag build and push normally, and `overwrite_from_index` is honored like any other branch.

### Divergent Tags (No Matching Branch)

If a request targets a tag with no corresponding Git branch — for example a timestamped, point-in-time tag cut from an existing index — IIB treats it as a **divergent tag**:

- Configs and `index.db` are extracted directly from the index image (unprivileged), not sourced from ORAS. Only the FBC configs and the hidden `index.db` are extracted; if the image carries no hidden `index.db`, the request fails ("no index.db found, onboard the image to build") — there is no labeled-db or empty-db fallback.
- The build reuses the base OCP branch's existing Konflux Component via a throw-away merge request.
- That MR is **never merged** — it is always closed after the pipeline completes (or on failure), regardless of outcome.
- `overwrite_from_index` is **rejected** for divergent-tag requests; there is no direct-push path.

This lets IIB build and validate a one-off tag without requiring per-tag branch/Component provisioning.

## Index DB Artifact and ImageStream Tag Naming

Cached `index.db` artifact tags (ORAS) and ImageStream tags are keyed on the index image's content (manifest) digest — `idb-<sha256>` — resolved via `skopeo inspect`, not on its pullspec. Because the key is the content itself, it is both namespace-safe and promotion-safe:

- **Namespace-safe:** two images that share a repository name in different registry namespaces (e.g. `quay.io/redhat/my-index:v4.17` vs `quay.io/redhat-pending/my-index:v4.17`) have different content and therefore different digests, so they never collide on the same cache tag.
- **Promotion-safe:** the same image content addressed by different pullspecs after a release or mirror (e.g. `quay.io/my-namespace/iib-pub:v4.17` → `registry.access.redhat.com/some-namespace/operator-index:v4.17`) preserves its manifest digest, so both pullspecs resolve to the *same* cache entry and share one `index.db`.

Cache entries written under the previous pullspec-derived naming scheme are orphaned by this change — they are not migrated in place. On the normal path IIB never falls back to extracting `index.db` from the image: if the digest-keyed artifact is missing, the request fails with a "no index.db found for the image, onboard the image to build" error, and the image must be onboarded (which populates the artifact) before it can be built. Orphaned entries are cleaned up by the existing cache-pruning process rather than any code path in this workflow.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[high] scope-creep

The content-key migration orphans every existing ORAS-cached index.db artifact in production. The README explicitly documents this: cached entries under the previous naming scheme are orphaned, and affected images fail with 'no index.db found' until re-onboarded. Every onboarded image will begin failing immediately after deployment with no fallback path or migration mechanism.

Suggested fix: Add an explicit deployment checklist item, or introduce a transient fallback that checks the digest-keyed artifact and, if absent, falls back to the old pullspec-keyed artifact for one release cycle.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is intentional. There are no production index.db atm


## Differences from Traditional Workflow

| Aspect | Traditional Workflow | Containerized Workflow |
Expand Down
9 changes: 9 additions & 0 deletions iib/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ class IIBError(BaseException):
"""An error was encountered in IIB."""


class FileNotFoundInImageError(IIBError):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] docstring-convention

All existing exception classes use single-sentence docstrings. FileNotFoundInImageError introduces a multi-paragraph rationale docstring, deviating from the established convention.

Suggested fix: Collapse to a single sentence; move rationale to an inline comment.

"""A requested path was not present in a container image.

Subclasses IIBError so existing ``except IIBError`` handlers still catch it,
while letting callers distinguish a genuinely absent path from a real
extraction failure (registry, OCI parsing, layer, or tar error).
"""


class ValidationError(BaseException):
"""Denote invalid input."""

Expand Down
2 changes: 1 addition & 1 deletion iib/workers/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class Config(object):
iib_index_db_oras_auth_path: str = os.path.join(
os.path.expanduser('~'), '.docker', 'oras', 'config.json'
)
iib_index_db_artifact_tag_template: str = '{image_name}-{tag}'
iib_index_db_artifact_tag_template: str = 'idb-{digest}'
Comment thread
lipoja marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] config-migration

The iib_index_db_artifact_tag_template default changed from '{image_name}-{tag}' to 'idb-{digest}'. Deployments customizing this config with old placeholders will get an immediate KeyError at runtime when .format(digest=...) is called -- a loud crash, not silent corruption.

Suggested fix: Add a startup validation check that the template contains '{digest}', or document the config migration in release notes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] breaking-config-interface

The format variable names in iib_index_db_artifact_tag_template changed from {image_name} and {tag} to {digest}. Any deployment overriding this setting in a site-level settings.py will get an unhandled KeyError at runtime when _get_artifact_combined_tag calls .format(digest=...) on a template referencing {image_name} and {tag}.

Suggested fix: Validate the config key's format string at startup (check that {digest} is present, warn if {image_name}/{tag} are found), or document that the template format is not operator-configurable.

iib_index_db_artifact_template: str = '{registry}/index-db:{tag}'
# Whether to use OpenShift ImageStream cache for index.db artifacts
# Requires OpenShift cluster with ImageStream configured
Expand Down
49 changes: 26 additions & 23 deletions iib/workers/tasks/build_containerized_add.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
)
from iib.workers.tasks.celery import app
from iib.workers.tasks.containerized_utils import (
prepare_git_repository_for_build,
prepare_build_sources,
fetch_and_verify_index_db_artifact,
write_build_metadata,
git_commit_and_create_mr,
Expand Down Expand Up @@ -148,38 +148,41 @@ def handle_containerized_add_request(
distribution_scope = prebuild_info['distribution_scope']

index_to_gitlab_push_map = index_to_gitlab_push_map or {}
# Variables mr_details, last_commit_sha and original_index_db_digest
# needs to be assigned; otherwise cleanup_on_failure() fails when an exception is raised.
# Variables mr_details and last_commit_sha need to be assigned; otherwise
# cleanup_on_failure() fails when an exception is raised.
mr_details: Optional[Dict[str, str]] = None
last_commit_sha: Optional[str] = None
original_index_db_digest: Optional[str] = None

Opm.set_opm_version(from_index_resolved)

_update_index_image_build_state(request_id, prebuild_info)
present_bundles: List[BundleImage] = []
present_bundles_pull_spec: List[str] = []
with tempfile.TemporaryDirectory(prefix=f'iib-{request_id}-') as temp_dir:
branch = prebuild_info['ocp_version']

# Set up and clone Git repository
(
index_git_repo,
local_git_repo_path,
localized_git_catalog_path,
) = prepare_git_repository_for_build(
sources = prepare_build_sources(
request_id=request_id,
from_index=str(from_index),
from_index_resolved=from_index_resolved,
temp_dir=temp_dir,
branch=branch,
ocp_version=prebuild_info['ocp_version'],
index_to_gitlab_push_map=index_to_gitlab_push_map,
overwrite_from_index=overwrite_from_index,
)

# Pull index.db artifact (uses ImageStream cache if configured, otherwise pulls directly)
artifact_index_db_file = fetch_and_verify_index_db_artifact(
from_index=str(from_index),
temp_dir=temp_dir,
)
index_git_repo = sources.index_git_repo
local_git_repo_path = sources.local_git_repo_path
localized_git_catalog_path = sources.localized_git_catalog_path
branch = sources.target_branch

# Divergent path already has index.db extracted from the image; the normal
# path pulls it from ORAS. NEVER fall back to ORAS on the divergent path —
# that would read the base OCP branch's index.db.
if sources.index_db_path is not None:
artifact_index_db_file = sources.index_db_path
else:
artifact_index_db_file = fetch_and_verify_index_db_artifact(
from_index=str(from_index),
temp_dir=temp_dir,
)

msg = 'Checking if bundles are already present in index image'
log.info(msg)
Expand Down Expand Up @@ -330,20 +333,21 @@ def handle_containerized_add_request(

# Push updated index.db before merging the MR so that on failure both
# git and the index.db artifact remain consistent (MR stays open,
# cleanup_on_failure closes it and rolls back the artifact).
original_index_db_digest = push_index_db_artifact(
# cleanup_on_failure closes it; the content-addressed artifact needs no rollback).
push_index_db_artifact(
request_id=request_id,
from_index=str(from_index),
index_db_path=artifact_index_db_file,
operators=operators,
output_image=image_url,
overwrite_from_index=overwrite_from_index,
request_type='add',
)

# Merge or close the MR as the final step so that all side effects
# (replication, metadata, index.db push) have succeeded before git
# is advanced. This prevents git/index.db divergence on partial failure.
if overwrite_from_index:
if overwrite_from_index and not sources.is_divergent:
merge_mr_after_build(mr_details, index_git_repo)
# Prevent cleanup_on_failure from trying to close an already-merged MR
mr_details = None
Expand All @@ -364,7 +368,6 @@ def handle_containerized_add_request(
request_id=request_id,
from_index=str(from_index),
index_repo_map=index_to_gitlab_push_map or {},
original_index_db_digest=original_index_db_digest,
reason=f"error: {e}",
)
raise IIBError(f"Failed to add bundles: {e}")
17 changes: 10 additions & 7 deletions iib/workers/tasks/build_containerized_create_empty_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
opm_validate,
)
from iib.workers.tasks.oras_utils import (
_get_artifact_combined_tag,
_get_name_and_tag_from_pullspec,
get_oras_artifact,
)
Expand Down Expand Up @@ -176,7 +175,6 @@ def handle_containerized_create_empty_index_request(
index_git_repo: Optional[str] = None
last_commit_sha: Optional[str] = None
output_pull_spec: Optional[str] = None
original_index_db_digest: Optional[str] = None

with tempfile.TemporaryDirectory(prefix=f'iib-{request_id}-') as temp_dir:
branch = ocp_version
Expand All @@ -199,11 +197,16 @@ def handle_containerized_create_empty_index_request(
conf = get_worker_config()
empty_tag = conf.get('iib_empty_index_db_tag', 'empty')

# Construct the pullspec for the empty index.db artifact
# Construct the pullspec for the empty index.db artifact.
# This tag intentionally does NOT use the content-digest key that
# _get_artifact_combined_tag derives for real indexes: the empty index.db
# is a shared, content-free seed artifact keyed only by image name +
# "empty", so it is deliberately reusable across indexes rather than tied
# to any single image's manifest digest.
image_name, _ = _get_name_and_tag_from_pullspec(from_index)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] API-shape-inconsistency

The create_empty_index handler bypasses _get_artifact_combined_tag by inlining a hardcoded format string. This is intentionally different (the empty artifact is a shared, namespace-agnostic sentinel), but the code lacks an explanatory comment. A future maintainer could fix this and break the shared semantics.

Suggested fix: Add an inline comment explaining that the empty-tag artifact is intentionally shared across namespaces and should not include the pullspec hash.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] data-integrity

Empty index.db artifact tag intentionally omits pullspec hash (shared namespace-agnostic seed). Documented design decision.

empty_artifact_ref = conf['iib_index_db_artifact_template'].format(
registry=conf['iib_index_db_artifact_registry'],
tag=_get_artifact_combined_tag(image_name, empty_tag),
tag=f"{image_name}-{empty_tag}",
)

log.info('Fetching empty index.db from %s', empty_artifact_ref)
Expand Down Expand Up @@ -344,12 +347,13 @@ def handle_containerized_create_empty_index_request(

# Push the empty index.db with request ID tag
# Since overwrite_from_index is False, this will only push with request_id tag
# and will not overwrite the v4.x tag
original_index_db_digest = push_index_db_artifact(
# and will not overwrite the current artifact
push_index_db_artifact(
request_id=request_id,
from_index=from_index,
index_db_path=str(index_db_path),
operators=[], # Empty list since we're creating an empty index
output_image=image_url,
overwrite_from_index=False, # Always False for create_empty_index
request_type='create_empty_index',
)
Expand All @@ -372,7 +376,6 @@ def handle_containerized_create_empty_index_request(
request_id=request_id,
from_index=from_index,
index_repo_map=index_to_gitlab_push_map or {},
original_index_db_digest=original_index_db_digest,
reason=f"error: {e}",
)
raise IIBError(f"Failed to create empty index: {e}")
Expand Down
47 changes: 25 additions & 22 deletions iib/workers/tasks/build_containerized_fbc_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
fetch_and_verify_index_db_artifact,
git_commit_and_create_mr,
monitor_pipeline_and_extract_image,
prepare_git_repository_for_build,
prepare_build_sources,
push_index_db_artifact,
replicate_image_to_tagged_destinations,
write_build_metadata,
Expand Down Expand Up @@ -112,11 +112,10 @@ def handle_containerized_fbc_operation_request(
distribution_scope = prebuild_info['distribution_scope']

index_to_gitlab_push_map = index_to_gitlab_push_map or {}
# Variables mr_details, last_commit_sha and original_index_db_digest
# needs to be assigned; otherwise cleanup_on_failure() fails when an exception is raised.
# Variables mr_details and last_commit_sha need to be assigned; otherwise
# cleanup_on_failure() fails when an exception is raised.
mr_details: Optional[Dict[str, str]] = None
last_commit_sha: Optional[str] = None
original_index_db_digest: Optional[str] = None

Opm.set_opm_version(from_index_resolved)

Expand All @@ -131,26 +130,30 @@ def handle_containerized_fbc_operation_request(
_update_index_image_build_state(request_id, prebuild_info)

with tempfile.TemporaryDirectory(prefix=f'iib-{request_id}-') as temp_dir:
branch = prebuild_info['ocp_version']

# Set up and clone Git repository
(
index_git_repo,
local_git_repo_path,
localized_git_catalog_path,
) = prepare_git_repository_for_build(
sources = prepare_build_sources(
request_id=request_id,
from_index=from_index,
from_index_resolved=from_index_resolved,
temp_dir=temp_dir,
branch=branch,
ocp_version=prebuild_info['ocp_version'],
index_to_gitlab_push_map=index_to_gitlab_push_map,
overwrite_from_index=overwrite_from_index,
)
index_git_repo = sources.index_git_repo
local_git_repo_path = sources.local_git_repo_path
localized_git_catalog_path = sources.localized_git_catalog_path
branch = sources.target_branch

# Pull index.db artifact (uses ImageStream cache if configured, otherwise pulls directly)
artifact_index_db_file = fetch_and_verify_index_db_artifact(
from_index=from_index,
temp_dir=temp_dir,
)
# Divergent path already has index.db extracted from the image; the normal
# path pulls it from ORAS. NEVER fall back to ORAS on the divergent path —
# that would read the base OCP branch's index.db.
if sources.index_db_path is not None:
artifact_index_db_file = sources.index_db_path
else:
artifact_index_db_file = fetch_and_verify_index_db_artifact(
from_index=from_index,
temp_dir=temp_dir,
)

set_request_state(request_id, 'in_progress', 'Adding fbc fragment')
(
Expand Down Expand Up @@ -232,20 +235,21 @@ def handle_containerized_fbc_operation_request(

# Push updated index.db before merging the MR so that on failure both
# git and the index.db artifact remain consistent (MR stays open,
# cleanup_on_failure closes it and rolls back the artifact).
original_index_db_digest = push_index_db_artifact(
# cleanup_on_failure closes it; the content-addressed artifact needs no rollback).
push_index_db_artifact(
request_id=request_id,
from_index=from_index,
index_db_path=index_db_path,
operators=operators_in_db,
output_image=image_url,
overwrite_from_index=overwrite_from_index,
request_type='fbc_operations',
)

# Merge or close the MR as the final step so that all side effects
# (replication, metadata, index.db push) have succeeded before git
# is advanced. This prevents git/index.db divergence on partial failure.
if overwrite_from_index:
if overwrite_from_index and not sources.is_divergent:
merge_mr_after_build(mr_details, index_git_repo)
# Prevent cleanup_on_failure from trying to close an already-merged MR
mr_details = None
Expand All @@ -267,7 +271,6 @@ def handle_containerized_fbc_operation_request(
request_id=request_id,
from_index=from_index,
index_repo_map=index_to_gitlab_push_map or {},
original_index_db_digest=original_index_db_digest,
reason=f"error: {e}",
)
raise IIBError(f"Failed to add FBC fragment: {e}")
7 changes: 3 additions & 4 deletions iib/workers/tasks/build_containerized_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,6 @@ def handle_containerized_merge_request(
index_git_repo: Optional[str] = None
last_commit_sha: Optional[str] = None
output_pull_spec: Optional[str] = None
original_index_db_digest: Optional[str] = None

with tempfile.TemporaryDirectory(prefix=f'iib-{request_id}-') as temp_dir:
# Setup and clone Git repository
Expand Down Expand Up @@ -371,12 +370,13 @@ def handle_containerized_merge_request(

# Push updated index.db before merging the MR so that on failure both
# git and the index.db artifact remain consistent (MR stays open,
# cleanup_on_failure closes it and rolls back the artifact).
original_index_db_digest = push_index_db_artifact(
# cleanup_on_failure closes it; the content-addressed artifact needs no rollback).
push_index_db_artifact(
request_id=request_id,
from_index=effective_index_image,
index_db_path=source_index_db_path,
operators=operators_in_db,
output_image=image_url,
overwrite_from_index=overwrite_target_index,
request_type='merge',
)
Expand Down Expand Up @@ -407,7 +407,6 @@ def handle_containerized_merge_request(
request_id=request_id,
from_index=effective_index_image,
index_repo_map={},
original_index_db_digest=original_index_db_digest,
reason=f"error: {e}",
)
# Reset Docker config for the next request. This is a fail safe.
Expand Down
1 change: 0 additions & 1 deletion iib/workers/tasks/build_containerized_regenerate_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,6 @@ def handle_containerized_regenerate_bundle_request(
request_id=request_id,
from_index='', # No from_index for bundle regeneration
index_repo_map={},
original_index_db_digest=None, # No index.db for bundle regeneration
reason=f"error: {e}",
)
raise IIBError(f"Failed to regenerate bundle: {e}")
Loading
Loading