Skip to content

Conversation

@yashvardhannanavati
Copy link
Collaborator

@yashvardhannanavati yashvardhannanavati commented Nov 14, 2025

Refers to CLOUDDST-28865

Assisted-by: Cursor

Summary by Sourcery

Implement a new containerized workflow handler for removal requests, add supporting utilities for index.db artifact caching and Git/Celery integration, and enhance Konflux and ORAS task modules with retry logic, image URL extraction, and execution context improvements while updating configuration and tests.

New Features:

  • Introduce handle_containerized_rm_request Celery task to orchestrate containerized operator removal via GitLab, Konflux pipelines, and ORAS artifact management
  • Add get_pipelinerun_image_url function to extract IMAGE_URL from Tekton pipelinerun results with fallback support
  • Enhance find_pipelinerun with tenacity-based retry logic for delayed pipelinerun creation
  • Add containerized_utils module including pull_index_db_artifact, write_build_metadata, and cleanup_on_failure utilities

Enhancements:

  • Modify wait_for_pipeline_completion to return the pipelinerun status payload on completion
  • Extend push_oras_artifact to accept a cwd parameter for relative-path artifact validation and execution context
  • Expand IIB worker configuration with index.db artifact registry and ImageStream caching templates and bump OPM version mappings for new OCP releases

Tests:

  • Update test_find_pipelinerun_empty_result to expect IIBError after retry exhaustion
  • Add comprehensive unit tests for get_pipelinerun_image_url covering multiple result formats, whitespace stripping, fallback logic, and failure scenarios

Signed-off-by: Yashvardhan Nanavati <[email protected]>

Assisted-by: Cursor
Signed-off-by: Yashvardhan Nanavati <[email protected]>
@sourcery-ai
Copy link

sourcery-ai bot commented Nov 14, 2025

Reviewer's Guide

This PR introduces a new containerized remove-operators request handler that coordinates Git-based catalog updates and Konflux pipelines, enhances existing Konflux utilities with retry and image URL extraction logic, extends ORAS artifact tooling with working-directory support, and augments configuration with index.db cache settings.

Sequence diagram for containerized remove-operators request workflow

sequenceDiagram
    actor User
    participant "IIB Worker"
    participant "Git Repository"
    participant "Konflux Pipeline"
    participant "Registry"
    User->>"IIB Worker": Submit remove-operators request
    "IIB Worker"->>"Git Repository": Clone repo, remove operators, commit/push
    "IIB Worker"->>"Konflux Pipeline": Trigger pipeline via commit
    "IIB Worker"->>"Konflux Pipeline": Wait for pipeline completion
    "Konflux Pipeline"-->>"IIB Worker": Pipeline completes, provides IMAGE_URL
    "IIB Worker"->>"Registry": Copy built index image to output registry
    "IIB Worker"->>User: Notify completion
Loading

ER diagram for new and updated configuration fields for index.db cache

erDiagram
    CONFIG {
      string iib_index_db_artifact_registry
      string iib_index_db_imagestream_registry
      string iib_index_db_artifact_tag_template
      string iib_index_db_artifact_template
      boolean iib_use_imagestream_cache
    }
    CONFIG ||--o| "ORAS_ARTIFACT" : uses
    CONFIG ||--o| "IMAGESTREAM_CACHE" : configures
Loading

Class diagram for new containerized remove-operators handler and related utilities

classDiagram
    class build_containerized_rm {
        +handle_containerized_rm_request(operators, request_id, from_index, ...)
    }
    class containerized_utils {
        +pull_index_db_artifact(from_index, temp_dir)
        +write_build_metadata(local_repo_path, opm_version, ocp_version, distribution_scope, binary_image, request_id)
        +cleanup_on_failure(mr_details, last_commit_sha, index_git_repo, overwrite_from_index, request_id, from_index, index_repo_map, original_index_db_digest, reason)
    }
    class konflux_utils {
        +find_pipelinerun(commit_sha)
        +wait_for_pipeline_completion(pipelinerun_name, timeout)
        +get_pipelinerun_image_url(pipelinerun_name, run)
    }
    class oras_utils {
        +push_oras_artifact(artifact_ref, local_path, artifact_type, registry_auths, annotations, cwd)
        +refresh_indexdb_cache_for_image(index_image_pullspec)
        +get_imagestream_artifact_pullspec(from_index)
    }
    class Config {
        +iib_index_db_artifact_registry: str
        +iib_index_db_imagestream_registry: str
        +iib_index_db_artifact_tag_template: str
        +iib_index_db_artifact_template: str
        +iib_use_imagestream_cache: bool
    }
    build_containerized_rm --> containerized_utils
    build_containerized_rm --> konflux_utils
    build_containerized_rm --> oras_utils
    build_containerized_rm --> Config
    containerized_utils --> oras_utils
    containerized_utils --> Config
    konflux_utils --> Config
    oras_utils --> Config
Loading

File-Level Changes

Change Details Files
Add retry mechanism and error handling to pipelinerun lookup
  • Apply tenacity retry decorator using configured attempts and backoff
  • Raise IIBError on empty results to trigger retries
  • Return full run object in wait_for_pipeline_completion
  • Update all and docstrings accordingly
iib/workers/tasks/konflux_utils.py
Implement IMAGE_URL extraction helper with tests
  • Create get_pipelinerun_image_url with results/pipelineResults support
  • Strip whitespace and log extraction
  • Raise IIBError when URL missing or invalid
  • Add parametrized tests to cover success and error scenarios
iib/workers/tasks/konflux_utils.py
tests/test_workers/test_tasks/test_konflux_utils.py
Enhance ORAS artifact push with cwd support
  • Introduce optional cwd parameter and validate full path
  • Log working directory usage
  • Pass cwd via params to run_cmd
iib/workers/tasks/oras_utils.py
Add containerized RM request handler and utilities
  • Implement handle_containerized_rm_request orchestrating Git clone, catalog removal, MR/commit and Konflux pipeline
  • Add pull_index_db_artifact with ImageStream cache logic
  • Add write_build_metadata and cleanup_on_failure utilities
iib/workers/tasks/build_containerized_rm.py
iib/workers/tasks/containerized_utils.py
Extend worker configuration for index.db caching
  • Add settings for index-db artifact registry, imagestream registry, templates
  • Introduce flag to enable ImageStream cache
  • Bump OPM version mappings in DevelopmentConfig
iib/workers/config.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • The handle_containerized_rm_request task is over 400 lines long—consider breaking it into smaller, focused helper functions to improve readability and testability.
  • The @Retry decorator on find_pipelinerun captures get_worker_config() at import time, so runtime changes to retry settings won’t apply—use lazy callables or a decorator factory to fetch config dynamically.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The handle_containerized_rm_request task is over 400 lines long—consider breaking it into smaller, focused helper functions to improve readability and testability.
- The @retry decorator on find_pipelinerun captures get_worker_config() at import time, so runtime changes to retry settings won’t apply—use lazy callables or a decorator factory to fetch config dynamically.

## Individual Comments

### Comment 1
<location> `iib/workers/tasks/oras_utils.py:213` </location>
<code_context>
+    refresh_indexdb_cache(_get_artifact_combined_tag(index_name, tag))
+
+
+def get_imagestream_artifact_pullspec(from_index: str) -> str:
+    """
+    Get the ImageStream pullspec for the index.db artifact.
</code_context>

<issue_to_address>
**suggestion (bug_risk):** get_imagestream_artifact_pullspec uses config keys without validation.

Add checks or default values for 'iib_index_db_artifact_template' and 'iib_index_db_imagestream_registry' to prevent KeyError if they are missing from the config.
</issue_to_address>

### Comment 2
<location> `iib/workers/config.py:45-51` </location>
<code_context>
     }
     iib_opm_pprof_lock_required_min_version = "1.29.0"
     iib_image_push_template: str = '{registry}/iib-build:{request_id}'
+    iib_index_db_artifact_registry: str = None
+    iib_index_db_imagestream_registry: str = None
+    iib_index_db_artifact_tag_template: str = '{image_name}-{tag}'
+    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
+    iib_use_imagestream_cache: bool = False
     iib_index_image_output_registry: Optional[str] = None
     iib_index_configs_gitlab_tokens_map: Optional[Dict[str, Dict[str, str]]] = None
</code_context>

<issue_to_address>
**suggestion (bug_risk):** New config options default to None, which may cause runtime errors.

Please set appropriate default values or add validation to prevent runtime errors when these config options are accessed.

Suggested implementation:

```python
    # Default registry for index.db artifacts (set to empty string if not configured)
    iib_index_db_artifact_registry: str = ''
    # Default registry for index.db ImageStream (set to empty string if not configured)
    iib_index_db_imagestream_registry: str = ''
    iib_index_db_artifact_tag_template: str = '{image_name}-{tag}'
    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
    iib_use_imagestream_cache: bool = False

```

If these config options are accessed elsewhere in the code, you should add validation logic such as:
```python
if not config.iib_index_db_artifact_registry:
    raise ValueError("iib_index_db_artifact_registry must be set in the configuration.")
```
wherever they are used and a value is required. Adjust the default values if you have a known default registry or imagestream registry.
</issue_to_address>

### Comment 3
<location> `iib/workers/tasks/build_containerized_rm.py:178-182` </location>
<code_context>
+
+        # Remove operators from /configs
+        set_request_state(request_id, 'in_progress', 'Removing operators from catalog')
+        for operator in operators:
+            operator_path = os.path.join(localized_git_catalog_path, operator)
+            if os.path.exists(operator_path):
+                log.debug('Removing operator from catalog: %s', operator_path)
+                shutil.rmtree(operator_path)
</code_context>

<issue_to_address>
**suggestion:** Operator removal does not handle missing operator directories.

Consider logging a warning or raising an error when an operator directory is missing, if silent ignoring is not desired.

```suggestion
        for operator in operators:
            operator_path = os.path.join(localized_git_catalog_path, operator)
            if os.path.exists(operator_path):
                log.debug('Removing operator from catalog: %s', operator_path)
                shutil.rmtree(operator_path)
            else:
                log.warning('Operator directory not found and cannot be removed: %s', operator_path)
```
</issue_to_address>

### Comment 4
<location> `iib/workers/tasks/containerized_utils.py:36-44` </location>
<code_context>
+    )
+
+    conf = get_worker_config()
+    if conf.get('iib_use_imagestream_cache', False):
+        # Verify index.db cache is synced. Refresh if not.
+        log.info('ImageStream cache is enabled. Checking cache sync status.')
+        cache_synced = verify_indexdb_cache_for_image(from_index)
+        if cache_synced:
+            log.info('Index.db cache is synced. Pulling from ImageStream.')
+            # Pull from ImageStream when digests match
+            imagestream_ref = get_imagestream_artifact_pullspec(from_index)
+            artifact_dir = get_oras_artifact(
+                imagestream_ref,
+                temp_dir,
</code_context>

<issue_to_address>
**suggestion (bug_risk):** ImageStream cache logic may not handle race conditions in cache refresh.

If refresh_indexdb_cache_for_image is asynchronous, a race condition may occur. Add a check to confirm the cache is updated before pulling from Quay.
</issue_to_address>

### Comment 5
<location> `iib/workers/tasks/containerized_utils.py:163-172` </location>
<code_context>
+    if original_index_db_digest:
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Restoring index.db artifact assumes oras copy will always succeed.

If oras copy fails due to registry issues or an invalid digest, the restoration may fail without notice. Please add error handling or alerts for unsuccessful restoration attempts.

Suggested implementation:

```python
    if original_index_db_digest:
        log.info("Restoring index.db artifact to original digest due to %s", reason)
        try:
            from iib.workers.tasks.oras_utils import get_indexdb_artifact_pullspec
            from iib.workers.tasks.utils import run_cmd

            # Get the v4.x artifact reference
            v4x_artifact_ref = get_indexdb_artifact_pullspec(from_index)

            # Extract registry and repository from the pullspec
            # Format: quay.io/namespace/repo:tag -> we need quay.io/namespace/repo

            # Attempt to restore index.db artifact using oras copy
            try:
                # Assuming oras_copy_cmd is constructed somewhere below
                result = run_cmd(oras_copy_cmd)
            except Exception as oras_error:
                log.error(
                    "Failed to restore index.db artifact with oras copy for digest %s: %s",
                    original_index_db_digest,
                    oras_error,
                )
                # Optionally, you can raise the error or handle it as needed
        except Exception as restore_error:
            log.error("Exception during index.db restoration setup: %s", restore_error)

```

- Make sure `oras_copy_cmd` is defined and used in the restoration logic. If it's not present in the visible code, ensure the oras copy command is constructed and executed within the try/except block.
- Adjust error handling (e.g., raising exceptions or returning error codes) as appropriate for your application's needs.
</issue_to_address>

### Comment 6
<location> `tests/test_workers/test_tasks/test_konflux_utils.py:76-79` </location>
<code_context>
-
-    # Verify
-    assert result == []
+    # Test & Verify - After retries exhausted, IIBError gets wrapped in generic error
+    # The original "No pipelinerun found" error triggers retries,
+    # then final attempt gets caught by Exception handler
+    with pytest.raises(IIBError, match="Unexpected error while fetching pipelineruns for commit abc123: IIBError"):
+        find_pipelinerun("abc123")

</code_context>

<issue_to_address>
**suggestion (testing):** Test now expects IIBError after retries, but does not verify retry count or timing.

Add assertions or use mocks to confirm the retry logic triggers the expected number of attempts and adheres to the configured timing, ensuring the mechanism functions correctly beyond just raising the error.

Suggested implementation:

```python
    mock_config.iib_retry_multiplier = 1  # Reduced to make test faster
    mock_get_worker_config.return_value = mock_config

    mock_client.list_namespaced_custom_object.return_value = {"items": []}

    # Patch time.sleep to avoid actual delay and track calls
    import time
    from unittest import mock

    retry_attempts = []

    def fake_sleep(seconds):
        retry_attempts.append(seconds)

    with mock.patch("time.sleep", side_effect=fake_sleep):
        # Test & Verify - After retries exhausted, IIBError gets wrapped in generic error
        # The original "No pipelinerun found" error triggers retries,
        # then final attempt gets caught by Exception handler
        with pytest.raises(IIBError, match="Unexpected error while fetching pipelineruns for commit abc123: IIBError"):
            find_pipelinerun("abc123")

        # Assert retry logic: number of attempts and timing
        # Assuming default retry count is 3 (adjust if different in your config)
        assert len(retry_attempts) == mock_config.iib_retry_count
        # Optionally, check the actual sleep values if exponential backoff is used
        # assert retry_attempts == [mock_config.iib_retry_multiplier * (2 ** i) for i in range(mock_config.iib_retry_count)]

```

- If your retry count or multiplier is set elsewhere, adjust the assertion accordingly.
- If the retry logic uses a different backoff formula, update the sleep value assertion to match.
- Ensure `mock_config.iib_retry_count` is set to the expected number of retries for the test.
</issue_to_address>

### Comment 7
<location> `iib/workers/tasks/konflux_utils.py:259-261` </location>
<code_context>
    pipeline_results = status.get('results', [])
    if not pipeline_results:
        pipeline_results = status.get('pipelineResults', [])

</code_context>

<issue_to_address>
**suggestion (code-quality):** Use `or` for providing a fallback value ([`use-or-for-fallback`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/use-or-for-fallback))

```suggestion
    pipeline_results = status.get('results', []) or status.get('pipelineResults', [])

```

<br/><details><summary>Explanation</summary>Thanks to the flexibility of Python's `or` operator, you can use a single
assignment statement, even if a variable can retrieve its value from various
sources. This is shorter and easier to read than using multiple assignments with
`if not` conditions.
</details>
</issue_to_address>

### Comment 8
<location> `iib/workers/tasks/build_containerized_rm.py:417` </location>
<code_context>
@app.task
@request_logger
@instrument_tracing(
    span_name="workers.tasks.build.handle_containerized_rm_request",
    attributes=get_binary_versions(),
)
def handle_containerized_rm_request(
    operators: List[str],
    request_id: int,
    from_index: str,
    binary_image: Optional[str] = None,
    add_arches: Optional[Set[str]] = None,
    overwrite_from_index: bool = False,
    overwrite_from_index_token: Optional[str] = None,
    distribution_scope: Optional[str] = None,
    binary_image_config: Optional[Dict[str, Dict[str, str]]] = None,
    build_tags: Optional[List[str]] = None,
    index_to_gitlab_push_map: Optional[Dict[str, str]] = None,
) -> None:
    """
    Coordinate the work needed to remove the input operators using containerized workflow.

    This function uses Git-based workflows and Konflux pipelines instead of local builds.

    :param list operators: a list of strings representing the name of the operators to
        remove from the index image.
    :param int request_id: the ID of the IIB build request
    :param str from_index: the pull specification of the container image containing the index that
        the index image build will be based from.
    :param str binary_image: the pull specification of the container image where the opm binary
        gets copied from.
    :param set add_arches: the set of arches to build in addition to the arches ``from_index`` is
        currently built for.
    :param bool overwrite_from_index: if True, overwrite the input ``from_index`` with the built
        index image.
    :param str overwrite_from_index_token: the token used for overwriting the input
        ``from_index`` image. This is required to use ``overwrite_from_index``.
        The format of the token must be in the format "user:password".
    :param str distribution_scope: the scope for distribution of the index image, defaults to
        ``None``.
    :param dict binary_image_config: the dict of config required to identify the appropriate
        ``binary_image`` to use.
    :param list build_tags: List of tags which will be applied to intermediate index images.
    :param dict index_to_gitlab_push_map: the dict mapping index images (keys) to GitLab repos
        (values) in order to remove their catalogs from GitLab.
    :raises IIBError: if the index image build fails.
    """
    reset_docker_config()
    set_request_state(request_id, 'in_progress', 'Preparing request for build')

    # Prepare request
    prebuild_info = prepare_request_for_build(
        request_id,
        RequestConfigAddRm(
            _binary_image=binary_image,
            from_index=from_index,
            overwrite_from_index_token=overwrite_from_index_token,
            add_arches=add_arches,
            distribution_scope=distribution_scope,
            binary_image_config=binary_image_config,
        ),
    )

    from_index_resolved = prebuild_info['from_index_resolved']
    binary_image_resolved = prebuild_info['binary_image_resolved']
    ocp_version = prebuild_info['ocp_version']
    distribution_scope = prebuild_info['distribution_scope']
    arches = prebuild_info['arches']

    # Set OPM version
    Opm.set_opm_version(from_index_resolved)
    opm_version = Opm.opm_version

    _update_index_image_build_state(request_id, prebuild_info)

    mr_details: Optional[Dict[str, str]] = None
    local_git_repo_path: Optional[str] = None
    index_git_repo: Optional[str] = None
    operators_in_db: Set[str] = set()
    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:
        # Get Git repository information
        # Strip tag and digest from from_index to get base image name for lookup
        base_image_name = from_index.split('@')[0].rsplit(':', 1)[0]

        if not index_to_gitlab_push_map or base_image_name not in index_to_gitlab_push_map:
            raise IIBError(
                f"Git repository mapping not found for from_index: {from_index} "
                f"(base image: {base_image_name}). "
                "index_to_gitlab_push_map is required."
            )

        index_git_repo = index_to_gitlab_push_map[base_image_name]
        token_name, git_token = get_git_token(index_git_repo)
        branch = ocp_version

        # Clone Git repository
        set_request_state(request_id, 'in_progress', 'Cloning Git repository')
        local_git_repo_path = os.path.join(temp_dir, 'git', branch)
        os.makedirs(local_git_repo_path, exist_ok=True)

        clone_git_repo(index_git_repo, branch, token_name, git_token, local_git_repo_path)

        localized_git_catalog_path = os.path.join(local_git_repo_path, 'configs')
        if not os.path.exists(localized_git_catalog_path):
            raise IIBError(f"Catalogs directory not found in {local_git_repo_path}")

        # Remove operators from /configs
        set_request_state(request_id, 'in_progress', 'Removing operators from catalog')
        for operator in operators:
            operator_path = os.path.join(localized_git_catalog_path, operator)
            if os.path.exists(operator_path):
                log.debug('Removing operator from catalog: %s', operator_path)
                shutil.rmtree(operator_path)

        # Remove operator deprecations
        remove_operator_deprecations(
            from_index_configs_dir=localized_git_catalog_path, operators=operators
        )

        # Pull index.db artifact (uses ImageStream cache if configured, otherwise pulls directly)
        artifact_dir = pull_index_db_artifact(from_index, temp_dir)

        # Find the index.db file in the artifact
        index_db_path = os.path.join(artifact_dir, "index.db")
        if not os.path.exists(index_db_path):
            raise IIBError(f"Index.db file not found at {index_db_path}")

        # Check if operators exist in index.db and remove if present
        set_request_state(request_id, 'in_progress', 'Checking and removing from index database')
        operators_in_db, index_db_path_verified = verify_operators_exists(
            from_index=None,
            base_dir=temp_dir,
            operator_packages=operators,
            overwrite_from_index_token=overwrite_from_index_token,
            index_db_path=index_db_path,
        )

        # Use verified path or fall back to original
        if index_db_path_verified:
            index_db_path = index_db_path_verified

        if operators_in_db:
            log.info('Removing operators %s from index.db', operators_in_db)
            # Remove from index.db and migrate to FBC
            fbc_dir, _ = opm_registry_rm_fbc(
                base_dir=temp_dir,
                from_index=from_index_resolved,
                operators=list[str](operators_in_db),
                index_db_path=index_db_path,
            )

            # rename `catalog` directory because we need to use this name for
            # final destination of catalog (defined in Dockerfile)
            catalog_from_db = os.path.join(temp_dir, 'from_db')
            os.rename(fbc_dir, catalog_from_db)

            # Merge migrated FBC with existing FBC in Git repo
            # overwrite data in `catalog_from_index` by data from `catalog_from_db`
            # this adds changes on not opted in operators to final FBC
            log.info('Merging migrated catalog with Git catalog')
            merge_catalogs_dirs(catalog_from_db, localized_git_catalog_path)

        fbc_dir_path = os.path.join(temp_dir, 'catalog')
        # We need to regenerate file-based catalog because we merged changes
        if os.path.exists(fbc_dir_path):
            shutil.rmtree(fbc_dir_path)
        # Copy catalog to correct location expected in Dockerfile
        # Use copytree instead of move to preserve the configs directory in Git repo
        shutil.copytree(localized_git_catalog_path, fbc_dir_path)

        # Validate merged catalog
        set_request_state(request_id, 'in_progress', 'Validating catalog')
        opm_validate(fbc_dir_path)

        # Write build metadata to a file to be added with the commit
        set_request_state(request_id, 'in_progress', 'Writing build metadata')
        write_build_metadata(
            local_git_repo_path,
            opm_version,
            ocp_version,
            distribution_scope,
            binary_image_resolved,
            request_id,
        )

        try:
            # Commit changes and create PR or push directly
            set_request_state(request_id, 'in_progress', 'Committing changes to Git repository')
            log.info("Committing changes to Git repository. Triggering KONFLUX pipeline.")

            # Determine if this is a throw-away request (no overwrite_from_index_token)
            if not overwrite_from_index_token:
                # Create MR for throw-away requests
                mr_details = create_mr(
                    request_id=request_id,
                    local_repo_path=local_git_repo_path,
                    repo_url=index_git_repo,
                    branch=branch,
                    commit_message=f"IIB: Remove operators {', '.join(operators)} for request {request_id}",
                )
                log.info("Created merge request: %s", mr_details.get('mr_url'))
            else:
                # Push directly to branch
                commit_and_push(
                    request_id=request_id,
                    local_repo_path=local_git_repo_path,
                    repo_url=index_git_repo,
                    branch=branch,
                    commit_message=f"IIB: Remove operators {', '.join(operators)} for request {request_id}",
                )

            # Get commit SHA before waiting for pipeline (while temp directory still exists)
            last_commit_sha = get_last_commit_sha(local_repo_path=local_git_repo_path)

            # Wait for Konflux pipeline
            set_request_state(request_id, 'in_progress', 'Waiting on KONFLUX build')

            # find_pipelinerun has retry decorator to handle delays in pipelinerun creation
            pipelines = find_pipelinerun(last_commit_sha)

            # Get the first pipelinerun (should typically be only one)
            pipelinerun = pipelines[0]
            pipelinerun_name = pipelinerun.get('metadata', {}).get('name')
            if not pipelinerun_name:
                raise IIBError("Pipelinerun name not found in pipeline metadata")

            run = wait_for_pipeline_completion(pipelinerun_name)

            # Extract IMAGE_URL from pipelinerun results
            image_url = get_pipelinerun_image_url(pipelinerun_name, run)

            # Build list of output pull specs to copy to
            _tags = [str(request_id)]
            if build_tags:
                _tags.extend(build_tags)
            conf = get_worker_config()
            output_pull_specs = []
            for tag in _tags:
                output_pull_spec = conf['iib_image_push_template'].format(
                    registry=conf['iib_registry'], request_id=tag
                )
                output_pull_specs.append(output_pull_spec)

            # Copy built index from Konflux to all output pull specs
            set_request_state(request_id, 'in_progress', 'Copying built index to IIB registry')
            for spec in output_pull_specs:
                _skopeo_copy(
                    source=f'docker://{image_url}',
                    destination=f'docker://{spec}',
                    copy_all=True,
                    exc_msg=f'Failed to copy built index from Konflux to {spec}',
                )
                log.info("Successfully copied image to %s", spec)

            # Use the first output_pull_spec as the primary one for request updates
            output_pull_spec = output_pull_specs[0]
            # Update request with final output
            if not output_pull_spec:
                raise IIBError(
                    "output_pull_spec was not set. "
                    "This should not happen if the pipeline completed successfully."
                )

            # Send an empty index_repo_map because the Git repository is already updated with the changes
            _update_index_image_pull_spec(
                output_pull_spec=output_pull_spec,
                request_id=request_id,
                arches=arches,
                from_index=from_index,
                overwrite_from_index=overwrite_from_index,
                overwrite_from_index_token=overwrite_from_index_token,
                resolved_prebuild_from_index=from_index_resolved,
                add_or_rm=True,
                is_image_fbc=True,
                # Passing an empty index_repo_map is intentional. In IIB 1.0, if overwrite_from_index
                # token is given, we push to git by default at the end of a request. In IIB 2.Oh!,
                # the commit is pushed earlier to trigger a Konflux pipelinerun. So the old
                # workflow isn't needed.
                index_repo_map={},
                rm_operators=operators,
            )

            # Push updated index.db if overwrite_from_index_token is provided
            # We can push it directly from temp_dir since we're still inside the context manager
            # Do it as the last step to avoid rolling back the index.db file if the pipeline fails.
            if operators_in_db and index_db_path and os.path.exists(index_db_path):
                # Get directory and filename separately to push only the filename
                # This ensures ORAS extracts the file as just "index.db" without directory structure
                index_db_dir = os.path.dirname(index_db_path)
                index_db_filename = os.path.basename(index_db_path)
                log.info('Pushing from directory: %s, filename: %s', index_db_dir, index_db_filename)

                # Push with request_id tag irrespective of the overwrite_from_index_token
                set_request_state(request_id, 'in_progress', 'Pushing updated index database')
                image_name, tag = _get_name_and_tag_from_pullspec(from_index)
                request_artifact_ref = conf['iib_index_db_artifact_template'].format(
                    registry=conf['iib_index_db_artifact_registry'],
                    tag=f"{_get_artifact_combined_tag(image_name, tag)}-{request_id}",
                )
                artifact_refs = [request_artifact_ref]
                if overwrite_from_index_token:
                    # Get the current digest of v4.x tag before overwriting it
                    # This allows us to restore it if anything fails after the push
                    v4x_artifact_ref = get_indexdb_artifact_pullspec(from_index)
                    log.info('Capturing original digest of %s for potential rollback', v4x_artifact_ref)
                    original_index_db_digest = get_image_digest(v4x_artifact_ref)
                    log.info('Original index.db digest: %s', original_index_db_digest)
                    artifact_refs.append(v4x_artifact_ref)

                for artifact_ref in artifact_refs:
                    push_oras_artifact(
                        artifact_ref=artifact_ref,
                        local_path=index_db_filename,
                        cwd=index_db_dir,
                        annotations={
                            'request_id': str(request_id),
                            'operation': 'remove_operators',
                            'operators': ','.join(operators),
                        },
                    )
                    log.info('Pushed %s to registry', artifact_ref)

            # Close MR if it was opened
            if mr_details and index_git_repo:
                try:
                    close_mr(mr_details, index_git_repo)
                    log.info("Closed merge request: %s", mr_details.get('mr_url'))
                except IIBError as e:
                    log.warning("Failed to close merge request: %s", e)

            set_request_state(
                request_id,
                'complete',
                f"The operator(s) {', '.join(operators)} were successfully removed from the index image",
            )
        except Exception as e:
            cleanup_on_failure(
                mr_details=mr_details,
                last_commit_sha=last_commit_sha,
                index_git_repo=index_git_repo,
                overwrite_from_index=overwrite_from_index,
                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 remove operators: {e}")

    # Reset Docker config for the next request. This is a fail safe.
    reset_docker_config()

</code_context>

<issue_to_address>
**suggestion (code-quality):** We've found these issues:

- Explicitly raise from a previous error ([`raise-from-previous-error`](https://docs.sourcery.ai/Reference/Default-Rules/suggestions/raise-from-previous-error/))
- Low code quality found in handle\_containerized\_rm\_request - 11% ([`low-code-quality`](https://docs.sourcery.ai/Reference/Default-Rules/comments/low-code-quality/))

```suggestion
            raise IIBError(f"Failed to remove operators: {e}") from e
```

<br/><details><summary>Explanation</summary>
The quality score for this function is below the quality threshold of 25%.
This score is a combination of the method length, cognitive complexity and working memory.

How can you solve this?

It might be worth refactoring this function to make it shorter and more readable.

- Reduce the function length by extracting pieces of functionality out into
  their own functions. This is the most important thing you can do - ideally a
  function should be less than 10 lines.
- Reduce nesting, perhaps by introducing guard clauses to return early.
- Ensure that variables are tightly scoped, so that code using related concepts
  sits together within the function rather than being scattered.</details>
</issue_to_address>

### Comment 9
<location> `iib/workers/tasks/containerized_utils.py:39-40` </location>
<code_context>
def pull_index_db_artifact(from_index: str, temp_dir: str) -> str:
    """
    Pull index.db artifact from registry, using ImageStream cache if available.

    This function determines whether to use OpenShift ImageStream cache or pull directly
    from the registry based on the iib_use_imagestream_cache configuration.

    :param str from_index: The from_index pullspec
    :param str temp_dir: Temporary directory where the artifact will be extracted
    :return: Path to the directory containing the extracted artifact
    :rtype: str
    :raises IIBError: If the pull operation fails
    """
    from iib.workers.tasks.oras_utils import (
        get_indexdb_artifact_pullspec,
        get_imagestream_artifact_pullspec,
        get_oras_artifact,
        refresh_indexdb_cache_for_image,
        verify_indexdb_cache_for_image,
    )

    conf = get_worker_config()
    if conf.get('iib_use_imagestream_cache', False):
        # Verify index.db cache is synced. Refresh if not.
        log.info('ImageStream cache is enabled. Checking cache sync status.')
        cache_synced = verify_indexdb_cache_for_image(from_index)
        if cache_synced:
            log.info('Index.db cache is synced. Pulling from ImageStream.')
            # Pull from ImageStream when digests match
            imagestream_ref = get_imagestream_artifact_pullspec(from_index)
            artifact_dir = get_oras_artifact(
                imagestream_ref,
                temp_dir,
            )
        else:
            log.info('Index.db cache is not synced. Refreshing and pulling from Quay.')
            refresh_indexdb_cache_for_image(from_index)
            # Pull directly from Quay after triggering refresh
            artifact_ref = get_indexdb_artifact_pullspec(from_index)
            artifact_dir = get_oras_artifact(
                artifact_ref,
                temp_dir,
            )
    else:
        # Pull directly from Quay without ImageStream cache
        log.info('ImageStream cache is disabled. Pulling index.db artifact directly from registry.')
        artifact_ref = get_indexdb_artifact_pullspec(from_index)
        artifact_dir = get_oras_artifact(
            artifact_ref,
            temp_dir,
        )

    return artifact_dir

</code_context>

<issue_to_address>
**suggestion (code-quality):** Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))

```suggestion
        if cache_synced := verify_indexdb_cache_for_image(from_index):
```
</issue_to_address>

### Comment 10
<location> `iib/workers/tasks/containerized_utils.py:166` </location>
<code_context>
def cleanup_on_failure(
    mr_details: Optional[Dict[str, str]],
    last_commit_sha: Optional[str],
    index_git_repo: Optional[str],
    overwrite_from_index: bool,
    request_id: int,
    from_index: str,
    index_repo_map: Dict[str, str],
    original_index_db_digest: Optional[str] = None,
    reason: str = "error",
) -> None:
    """
    Clean up Git changes and index.db artifacts on failure.

    If a merge request was created, it will be closed (since the commit is only in a
    feature branch). If changes were pushed directly to the main branch, the commit
    will be reverted. If the index.db artifact was pushed to the v4.x tag, it will be
    restored to the original digest.

    :param Optional[Dict[str, str]] mr_details: Details of the merge request if one was created
    :param Optional[str] last_commit_sha: The SHA of the last commit
    :param Optional[str] index_git_repo: URL of the Git repository
    :param bool overwrite_from_index: Whether to overwrite the from_index
    :param int request_id: The IIB request ID
    :param str from_index: The from_index pullspec
    :param Dict[str, str] index_repo_map: Mapping of index images to Git repositories
    :param Optional[str] original_index_db_digest: Original digest of index.db before overwrite
    :param str reason: Reason for the cleanup (used in log messages)
    """
    if mr_details and index_git_repo:
        # If we created an MR, just close it (commit is only in feature branch)
        log.info("Closing merge request due to %s", reason)
        try:
            from iib.workers.tasks.git_utils import close_mr

            close_mr(mr_details, index_git_repo)
            log.info("Closed merge request: %s", mr_details.get('mr_url'))
        except Exception as close_error:
            log.warning("Failed to close merge request: %s", close_error)
    elif overwrite_from_index and last_commit_sha:
        # If we pushed directly, revert the commit
        log.error("Reverting commit due to %s", reason)
        try:
            from iib.workers.tasks.git_utils import revert_last_commit

            revert_last_commit(
                request_id=request_id,
                from_index=from_index,
                index_repo_map=index_repo_map,
            )
        except Exception as revert_error:
            log.error("Failed to revert commit: %s", revert_error)
    else:
        log.error("Neither MR nor commit to revert. No cleanup needed for %s", reason)

    # Restore index.db artifact to original digest if it was overwritten
    if original_index_db_digest:
        log.info("Restoring index.db artifact to original digest due to %s", reason)
        try:
            from iib.workers.tasks.oras_utils import get_indexdb_artifact_pullspec
            from iib.workers.tasks.utils import run_cmd

            # Get the v4.x artifact reference
            v4x_artifact_ref = get_indexdb_artifact_pullspec(from_index)

            # Extract registry and repository from the pullspec
            # Format: quay.io/namespace/repo:tag -> we need quay.io/namespace/repo
            artifact_name = v4x_artifact_ref.rsplit(':', 1)[0]

            # Use oras copy to restore the old digest to v4.x tag
            # This is a registry-to-registry copy, no download needed
            source_ref = f'{artifact_name}@{original_index_db_digest}'
            log.info("Restoring %s from %s", v4x_artifact_ref, source_ref)

            run_cmd(
                ['oras', 'copy', source_ref, v4x_artifact_ref],
                exc_msg=f'Failed to restore index.db artifact from {source_ref} to {v4x_artifact_ref}',
            )
            log.info("Successfully restored index.db artifact to original digest")
        except Exception as restore_error:
            log.error("Failed to restore index.db artifact: %s", restore_error)

</code_context>

<issue_to_address>
**issue (code-quality):** Extract code out into function ([`extract-method`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/extract-method/))
</issue_to_address>

### Comment 11
<location> `iib/workers/tasks/konflux_utils.py:134` </location>
<code_context>
@retry(
    before_sleep=before_sleep_log(log, logging.WARNING),
    reraise=True,
    retry=retry_if_exception_type(IIBError),
    stop=stop_after_attempt(get_worker_config().iib_total_attempts),
    wait=wait_chain(wait_exponential(multiplier=get_worker_config().iib_retry_multiplier)),
)
def find_pipelinerun(commit_sha: str) -> List[Dict[str, Any]]:
    """
    Find the Konflux pipelinerun triggered by the git commit.

    This function will retry if no pipelineruns are found (empty list), as it may take
    a few seconds for the pipelinerun to start after a commit is pushed.

    :param str commit_sha: The git commit SHA to search for
    :return: List of pipelinerun objects matching the commit SHA
    :rtype: List[Dict[str, Any]]
    :raises IIBError: If there's an error fetching pipelineruns or no pipelineruns found after retries
    """
    try:
        log.info("Searching for pipelineruns with commit SHA: %s", commit_sha)

        v1_client = _get_kubernetes_client()
        worker_config = get_worker_config()
        namespace = worker_config.iib_konflux_namespace

        runs = v1_client.list_namespaced_custom_object(
            group="tekton.dev",
            version="v1",
            namespace=namespace,
            plural="pipelineruns",
            label_selector=f"pipelinesascode.tekton.dev/sha={commit_sha}",
        )

        items = runs.get("items", [])
        log.info("Found %s pipelinerun(s) for commit %s", len(items), commit_sha)

        # Raise IIBError if no pipelineruns found to trigger retry
        if not items:
            raise IIBError(f"No pipelinerun found for commit SHA: {commit_sha}")

        return items

    except ApiException as e:
        error_msg = f"Failed to fetch pipelineruns for commit {commit_sha}: API error {e.status}"
        log.error("Kubernetes API error while fetching pipelineruns: %s - %s", e.status, e.reason)
        raise IIBError(error_msg)
    except Exception as e:
        error_msg = (
            f"Unexpected error while fetching pipelineruns for commit {commit_sha}: "
            f"{type(e).__name__}"
        )
        log.error("Unexpected error while fetching pipelineruns: %s", type(e).__name__)
        raise IIBError(error_msg)

</code_context>

<issue_to_address>
**issue (code-quality):** Explicitly raise from a previous error [×2] ([`raise-from-previous-error`](https://docs.sourcery.ai/Reference/Default-Rules/suggestions/raise-from-previous-error/))
</issue_to_address>

### Comment 12
<location> `iib/workers/tasks/konflux_utils.py:267-268` </location>
<code_context>
def get_pipelinerun_image_url(pipelinerun_name: str, run: Dict[str, Any]) -> str:
    """
    Extract IMAGE_URL from a completed pipelinerun's results.

    :param str pipelinerun_name: Name of the pipelinerun
    :param Dict[str, Any] run: The pipelinerun object
    :return: The IMAGE_URL value from the pipelinerun results
    :rtype: str
    :raises IIBError: If IMAGE_URL is not found in the pipelinerun results
    """
    status = run.get('status', {})

    # Check for 'results' (Konflux format) first, then fall back to 'pipelineResults' (older Tekton)
    pipeline_results = status.get('results', [])
    if not pipeline_results:
        pipeline_results = status.get('pipelineResults', [])

    log.info("Found %d pipeline results for %s", len(pipeline_results), pipelinerun_name)

    for result in pipeline_results:
        if result.get('name') == 'IMAGE_URL':
            image_url = result.get('value')
            if image_url:
                # Strip whitespace (including newlines) from the URL
                image_url = image_url.strip()
                log.info("Extracted IMAGE_URL from pipelinerun %s: %s", pipelinerun_name, image_url)
                return image_url

    # If not found, log for debugging
    log.error("IMAGE_URL not found in pipelinerun %s. Available results: %s",
              pipelinerun_name, [r.get('name') for r in pipeline_results])
    raise IIBError(f"IMAGE_URL not found in pipelinerun {pipelinerun_name} results")

</code_context>

<issue_to_address>
**suggestion (code-quality):** Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))

```suggestion
            if image_url := result.get('value'):
```
</issue_to_address>

### Comment 13
<location> `iib/workers/tasks/oras_utils.py:116` </location>
<code_context>
@instrument_tracing(span_name="workers.tasks.oras_utils.push_oras_artifact")
def push_oras_artifact(
    artifact_ref: str,
    local_path: str,
    artifact_type: str = "application/vnd.sqlite",
    registry_auths: Optional[Dict[str, Any]] = None,
    annotations: Optional[Dict[str, str]] = None,
    cwd: Optional[str] = None,
) -> None:
    """
    Push a local artifact to an OCI registry using ORAS.

    This function is equivalent to: `oras push {artifact_ref} {local_path}:{artifact_type}`

    :param str artifact_ref: OCI artifact reference to push to (e.g., 'quay.io/repo/repo:tag')
    :param str local_path: Local path to the artifact file. Should be a relative path.
        When using cwd, this should be a relative path (typically just
        the filename) relative to the cwd directory.
    :param str artifact_type: MIME type of the artifact (default: 'application/vnd.sqlite')
    :param dict registry_auths: Optional dockerconfig.json auth information for private registries
    :param dict annotations: Optional annotations to add to the artifact
    :param str cwd: Optional working directory for the ORAS command. When provided, local_path
        should be relative to this directory (e.g., just the filename).
    :raises IIBError: If the push operation fails
    """
    log.info('Pushing artifact from %s to %s with type %s', local_path, artifact_ref, artifact_type)
    if cwd:
        log.info('Using working directory: %s', cwd)

    # Construct the full path for validation
    full_path = os.path.join(cwd, local_path) if cwd else local_path
    if not os.path.exists(full_path):
        raise IIBError(f'Local artifact path does not exist: {full_path}')

    # Build ORAS push command
    cmd = ['oras', 'push', artifact_ref, f'{local_path}:{artifact_type}']

    # Add --disable-path-validation flag for absolute paths
    if os.path.isabs(local_path):
        cmd.append('--disable-path-validation')

    # Add annotations if provided
    if annotations:
        for key, value in annotations.items():
            cmd.extend(['--annotation', f'{key}={value}'])

    # Prepare parameters for run_cmd
    params = {}
    if cwd:
        params['cwd'] = cwd

    # Use namespace-specific registry authentication if provided
    with set_registry_auths(registry_auths, use_empty_config=True):
        try:
            run_cmd(cmd, params=params, exc_msg=f'Failed to push OCI artifact to {artifact_ref}')
            log.info('Successfully pushed OCI artifact to %s', artifact_ref)
        except Exception as e:
            raise IIBError(f'Failed to push OCI artifact to {artifact_ref}: {e}')

</code_context>

<issue_to_address>
**suggestion (code-quality):** Explicitly raise from a previous error ([`raise-from-previous-error`](https://docs.sourcery.ai/Reference/Default-Rules/suggestions/raise-from-previous-error/))

```suggestion
            raise IIBError(f'Failed to push OCI artifact to {artifact_ref}: {e}') from e
```
</issue_to_address>

### Comment 14
<location> `iib/workers/tasks/oras_utils.py:229-231` </location>
<code_context>
def get_imagestream_artifact_pullspec(from_index: str) -> str:
    """
    Get the ImageStream pullspec for the index.db artifact.

    This function constructs the internal OpenShift ImageStream pullspec that can be used
    to pull the index.db artifact from the cached ImageStream instead of directly from Quay.

    :param str from_index: The from_index pullspec
    :return: ImageStream pullspec for the artifact
    :rtype: str
    """
    conf = get_worker_config()
    image_name, tag = _get_name_and_tag_from_pullspec(from_index)
    combined_tag = _get_artifact_combined_tag(image_name, tag)

    # ImageStream pullspec format: image-registry.openshift-image-registry.svc:5000/{namespace}/index-db:{combined_tag}
    imagestream_pullspec = conf['iib_index_db_artifact_template'].format(
        registry=conf['iib_index_db_imagestream_registry'], tag=combined_tag
    )
    return imagestream_pullspec

</code_context>

<issue_to_address>
**issue (code-quality):** Inline variable that is immediately returned ([`inline-immediately-returned-variable`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/inline-immediately-returned-variable/))
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

refresh_indexdb_cache(_get_artifact_combined_tag(index_name, tag))


def get_imagestream_artifact_pullspec(from_index: str) -> str:
Copy link

Choose a reason for hiding this comment

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

suggestion (bug_risk): get_imagestream_artifact_pullspec uses config keys without validation.

Add checks or default values for 'iib_index_db_artifact_template' and 'iib_index_db_imagestream_registry' to prevent KeyError if they are missing from the config.

Comment on lines +45 to +51
iib_index_db_artifact_registry: str = None
iib_index_db_imagestream_registry: str = None
iib_index_db_artifact_tag_template: str = '{image_name}-{tag}'
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
iib_use_imagestream_cache: bool = False
Copy link

Choose a reason for hiding this comment

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

suggestion (bug_risk): New config options default to None, which may cause runtime errors.

Please set appropriate default values or add validation to prevent runtime errors when these config options are accessed.

Suggested implementation:

    # Default registry for index.db artifacts (set to empty string if not configured)
    iib_index_db_artifact_registry: str = ''
    # Default registry for index.db ImageStream (set to empty string if not configured)
    iib_index_db_imagestream_registry: str = ''
    iib_index_db_artifact_tag_template: str = '{image_name}-{tag}'
    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
    iib_use_imagestream_cache: bool = False

If these config options are accessed elsewhere in the code, you should add validation logic such as:

if not config.iib_index_db_artifact_registry:
    raise ValueError("iib_index_db_artifact_registry must be set in the configuration.")

wherever they are used and a value is required. Adjust the default values if you have a known default registry or imagestream registry.

Comment on lines +178 to +182
for operator in operators:
operator_path = os.path.join(localized_git_catalog_path, operator)
if os.path.exists(operator_path):
log.debug('Removing operator from catalog: %s', operator_path)
shutil.rmtree(operator_path)
Copy link

Choose a reason for hiding this comment

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

suggestion: Operator removal does not handle missing operator directories.

Consider logging a warning or raising an error when an operator directory is missing, if silent ignoring is not desired.

Suggested change
for operator in operators:
operator_path = os.path.join(localized_git_catalog_path, operator)
if os.path.exists(operator_path):
log.debug('Removing operator from catalog: %s', operator_path)
shutil.rmtree(operator_path)
for operator in operators:
operator_path = os.path.join(localized_git_catalog_path, operator)
if os.path.exists(operator_path):
log.debug('Removing operator from catalog: %s', operator_path)
shutil.rmtree(operator_path)
else:
log.warning('Operator directory not found and cannot be removed: %s', operator_path)

Comment on lines +36 to +44
if conf.get('iib_use_imagestream_cache', False):
# Verify index.db cache is synced. Refresh if not.
log.info('ImageStream cache is enabled. Checking cache sync status.')
cache_synced = verify_indexdb_cache_for_image(from_index)
if cache_synced:
log.info('Index.db cache is synced. Pulling from ImageStream.')
# Pull from ImageStream when digests match
imagestream_ref = get_imagestream_artifact_pullspec(from_index)
artifact_dir = get_oras_artifact(
Copy link

Choose a reason for hiding this comment

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

suggestion (bug_risk): ImageStream cache logic may not handle race conditions in cache refresh.

If refresh_indexdb_cache_for_image is asynchronous, a race condition may occur. Add a check to confirm the cache is updated before pulling from Quay.

Comment on lines +163 to +172
if original_index_db_digest:
log.info("Restoring index.db artifact to original digest due to %s", reason)
try:
from iib.workers.tasks.oras_utils import get_indexdb_artifact_pullspec
from iib.workers.tasks.utils import run_cmd

# Get the v4.x artifact reference
v4x_artifact_ref = get_indexdb_artifact_pullspec(from_index)

# Extract registry and repository from the pullspec
Copy link

Choose a reason for hiding this comment

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

suggestion (bug_risk): Restoring index.db artifact assumes oras copy will always succeed.

If oras copy fails due to registry issues or an invalid digest, the restoration may fail without notice. Please add error handling or alerts for unsuccessful restoration attempts.

Suggested implementation:

    if original_index_db_digest:
        log.info("Restoring index.db artifact to original digest due to %s", reason)
        try:
            from iib.workers.tasks.oras_utils import get_indexdb_artifact_pullspec
            from iib.workers.tasks.utils import run_cmd

            # Get the v4.x artifact reference
            v4x_artifact_ref = get_indexdb_artifact_pullspec(from_index)

            # Extract registry and repository from the pullspec
            # Format: quay.io/namespace/repo:tag -> we need quay.io/namespace/repo

            # Attempt to restore index.db artifact using oras copy
            try:
                # Assuming oras_copy_cmd is constructed somewhere below
                result = run_cmd(oras_copy_cmd)
            except Exception as oras_error:
                log.error(
                    "Failed to restore index.db artifact with oras copy for digest %s: %s",
                    original_index_db_digest,
                    oras_error,
                )
                # Optionally, you can raise the error or handle it as needed
        except Exception as restore_error:
            log.error("Exception during index.db restoration setup: %s", restore_error)
  • Make sure oras_copy_cmd is defined and used in the restoration logic. If it's not present in the visible code, ensure the oras copy command is constructed and executed within the try/except block.
  • Adjust error handling (e.g., raising exceptions or returning error codes) as appropriate for your application's needs.

if original_index_db_digest:
log.info("Restoring index.db artifact to original digest due to %s", reason)
try:
from iib.workers.tasks.oras_utils import get_indexdb_artifact_pullspec
Copy link

Choose a reason for hiding this comment

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

issue (code-quality): Extract code out into function (extract-method)

:raises IIBError: If there's an error fetching pipelineruns
:raises IIBError: If there's an error fetching pipelineruns or no pipelineruns found after retries
"""
try:
Copy link

Choose a reason for hiding this comment

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

issue (code-quality): Explicitly raise from a previous error [×2] (raise-from-previous-error)

Comment on lines +267 to +268
image_url = result.get('value')
if image_url:
Copy link

Choose a reason for hiding this comment

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

suggestion (code-quality): Use named expression to simplify assignment and conditional (use-named-expression)

Suggested change
image_url = result.get('value')
if image_url:
if image_url := result.get('value'):

run_cmd(cmd, params=params, exc_msg=f'Failed to push OCI artifact to {artifact_ref}')
log.info('Successfully pushed OCI artifact to %s', artifact_ref)
except Exception as e:
raise IIBError(f'Failed to push OCI artifact to {artifact_ref}: {e}')
Copy link

Choose a reason for hiding this comment

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

suggestion (code-quality): Explicitly raise from a previous error (raise-from-previous-error)

Suggested change
raise IIBError(f'Failed to push OCI artifact to {artifact_ref}: {e}')
raise IIBError(f'Failed to push OCI artifact to {artifact_ref}: {e}') from e

Comment on lines +229 to +231
imagestream_pullspec = conf['iib_index_db_artifact_template'].format(
registry=conf['iib_index_db_imagestream_registry'], tag=combined_tag
)
Copy link

Choose a reason for hiding this comment

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

issue (code-quality): Inline variable that is immediately returned (inline-immediately-returned-variable)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant