Skip to content

Conversation

lipoja
Copy link
Contributor

@lipoja lipoja commented Sep 16, 2025

[CLOUDDST-28870]

Summary by Sourcery

Introduce utility functions for computing container image digests and managing the OpenShift index-db-cache ImageStream cache.

New Features:

  • Add get_image_digest helper in workers.tasks.utils to compute image digests for both schema v1 and v2 manifests.
  • Add get_image_stream_digest in workers.tasks.oras_utils to retrieve ImageStream digests from OpenShift.
  • Add verify_indexdb_cache_sync to compare remote registry and ImageStream digests and report sync status.
  • Add refresh_indexdb_cache to import images into the index-db-cache ImageStream and force cache synchronization.

Enhancements:

  • Refactor get_resolved_image to delegate digest retrieval to the new get_image_digest function.

Tests:

  • Add unit tests for get_image_digest covering schema v2, schema v1, and invalid JSON scenarios.
  • Add tests for get_image_stream_digest, verify_indexdb_cache_sync, and refresh_indexdb_cache success and failure cases.

Copy link

sourcery-ai bot commented Sep 16, 2025

Reviewer's Guide

This PR introduces a centralized image digest utility based on skopeo and adds helper functions to manage the IndexDB ImageStream cache in OpenShift by wrapping oc CLI commands, along with comprehensive test coverage for both digest computation and cache synchronization methods.

Sequence diagram for index.db cache synchronization and refresh

sequenceDiagram
    participant User
    participant oras_utils
    participant utils
    participant OpenShift
    participant Quay

    User->>oras_utils: call verify_indexdb_cache_sync(tag)
    oras_utils->>utils: get_image_digest(repository:tag)
    utils->>Quay: skopeo_inspect(repository:tag)
    Quay-->>utils: return digest
    oras_utils->>OpenShift: get_image_stream_digest(tag)
    OpenShift-->>oras_utils: return digest
    oras_utils->>User: return True/False (cache synced?)

    User->>oras_utils: call refresh_indexdb_cache(tag, registry_auths)
    oras_utils->>OpenShift: import-image index-db-cache:tag from Quay repository:tag
    OpenShift-->>oras_utils: cache refreshed
Loading

Class diagram for new and updated helper functions in oras_utils.py and utils.py

classDiagram
    class oras_utils {
        +get_image_stream_digest(tag: str) str
        +verify_indexdb_cache_sync(tag: str) bool
        +refresh_indexdb_cache(tag: str, registry_auths: Optional[Dict[str, Any]]) None
    }
    class utils {
        +get_image_digest(pull_spec: str) str
        +get_resolved_image(pull_spec: str) str
    }
    oras_utils ..> utils : uses
    utils <|-- get_image_digest
    utils <|-- get_resolved_image
    oras_utils <|-- get_image_stream_digest
    oras_utils <|-- verify_indexdb_cache_sync
    oras_utils <|-- refresh_indexdb_cache
Loading

File-Level Changes

Change Details Files
Centralize image digest computation using skopeo in a new utility
  • Implement get_image_digest: raw inspect for schema v2 and fallback inspect for schema v1
  • Refactor get_resolved_image to delegate digest lookup to get_image_digest
  • Add unit tests covering schema v1, schema v2, and invalid JSON scenarios
iib/workers/tasks/utils.py
tests/test_workers/test_tasks/test_utils.py
Introduce IndexDB cache synchronization helpers in oras_utils
  • Add get_image_stream_digest to query ImageStream digest with oc
  • Add verify_indexdb_cache_sync to compare remote and local digests
  • Add refresh_indexdb_cache to re-import images with registry auth context
  • Refactor imports to include get_image_digest and add unit tests for success and failure paths
iib/workers/tasks/oras_utils.py
tests/test_workers/test_tasks/test_oras_utils.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

@lipoja lipoja force-pushed the image_stream_cache branch 5 times, most recently from 7127f12 to 103e971 Compare September 18, 2025 12:35
[CLOUDDST-28870]

Adding helper functions for index.db caching

Assisted by: Gemini

[CLOUDDST-28870]
@yashvardhannanavati yashvardhannanavati marked this pull request as ready for review September 22, 2025 19:20
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 and they look great!

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `iib/workers/tasks/oras_utils.py:120` </location>
<code_context>
+    :return: The image digest (e.g., "sha256:...").
+    :rtype: str
+    """
+    jsonpath = f'\'{{.status.tags[?(@.tag=="{tag}")].items[0].image}}\''
+    return run_cmd(
+        ['oc', 'get', 'imagestream', 'index-db-cache', '-o', f'jsonpath={jsonpath}'],
</code_context>

<issue_to_address>
**suggestion:** The jsonpath string uses single quotes which may cause issues with oc CLI.

Consider removing the single quotes from the jsonpath expression unless they are specifically required, as they may cause parsing issues with run_cmd.

```suggestion
    jsonpath = f'{{.status.tags[?(@.tag=="{tag}")].items[0].image}}'
```
</issue_to_address>

### Comment 2
<location> `tests/test_workers/test_tasks/test_oras_utils.py:334-335` </location>
<code_context>
     assert 'user:pass' not in all_messages  # decoded credentials
+
+
[email protected]('iib.workers.tasks.oras_utils.run_cmd')
+def test_get_image_stream_digest(mock_run_cmd):
+    """Test successful retrieval of image digest from ImageStream."""
+    mock_run_cmd.return_value = 'sha256:12345'
</code_context>

<issue_to_address>
**suggestion (testing):** Consider testing get_image_stream_digest with unexpected output formats.

Please add tests for cases where run_cmd returns values like an empty string, None, or a non-digest value to verify error handling in get_image_stream_digest.

Suggested implementation:

```python
@mock.patch('iib.workers.tasks.oras_utils.run_cmd')
def test_get_image_stream_digest(mock_run_cmd):
    """Test successful retrieval of image digest from ImageStream."""
    mock_run_cmd.return_value = 'sha256:12345'
    tag = 'test-tag'

    digest = get_image_stream_digest(tag)

    assert digest == 'sha256:12345'
    mock_run_cmd.assert_called_once_with(
        [
            'oc',
            'get',
            'imagestream',
        ]
    )

@mock.patch('iib.workers.tasks.oras_utils.run_cmd')
def test_get_image_stream_digest_empty_string(mock_run_cmd):
    """Test get_image_stream_digest with empty string output."""
    mock_run_cmd.return_value = ''
    tag = 'test-tag'
    try:
        digest = get_image_stream_digest(tag)
    except Exception as exc:
        assert isinstance(exc, Exception)
    else:
        assert digest is None or digest == '', "Expected None or empty digest for empty string output"

@mock.patch('iib.workers.tasks.oras_utils.run_cmd')
def test_get_image_stream_digest_none(mock_run_cmd):
    """Test get_image_stream_digest with None output."""
    mock_run_cmd.return_value = None
    tag = 'test-tag'
    try:
        digest = get_image_stream_digest(tag)
    except Exception as exc:
        assert isinstance(exc, Exception)
    else:
        assert digest is None, "Expected None digest for None output"

@mock.patch('iib.workers.tasks.oras_utils.run_cmd')
def test_get_image_stream_digest_invalid_format(mock_run_cmd):
    """Test get_image_stream_digest with non-digest output."""
    mock_run_cmd.return_value = 'not-a-digest'
    tag = 'test-tag'
    try:
        digest = get_image_stream_digest(tag)
    except Exception as exc:
        assert isinstance(exc, Exception)
    else:
        assert digest is None or digest == 'not-a-digest', "Expected None or raw output for invalid digest format"

```

You may need to adjust the assertions in the new tests depending on how `get_image_stream_digest` is implemented (e.g., whether it raises exceptions or returns `None` for invalid output). If it raises a specific exception, replace `Exception` with the specific exception type.
</issue_to_address>

### Comment 3
<location> `tests/test_workers/test_tasks/test_oras_utils.py:363-365` </location>
<code_context>
+        get_image_stream_digest('test-tag')
+
+
[email protected]('iib.workers.tasks.oras_utils.get_image_stream_digest')
[email protected]('iib.workers.tasks.oras_utils.get_image_digest')
+def test_verify_indexdb_cache_sync_match(mock_get_image_digest, mock_get_is_digest):
+    """Test successful verification when digests match."""
+    mock_get_image_digest.return_value = 'sha256:abc'
</code_context>

<issue_to_address>
**suggestion (testing):** Consider adding a test for verify_indexdb_cache_sync when get_image_digest or get_image_stream_digest raises an exception.

Please add a test to verify that verify_indexdb_cache_sync properly handles exceptions from get_image_digest or get_image_stream_digest.
</issue_to_address>

### Comment 4
<location> `tests/test_workers/test_tasks/test_oras_utils.py:393-397` </location>
<code_context>
+    mock_get_is_digest.assert_called_once_with(tag)
+
+
[email protected]('iib.workers.tasks.oras_utils.set_registry_auths')
[email protected]('iib.workers.tasks.oras_utils.run_cmd')
+def test_refresh_indexdb_cache_success(mock_run_cmd, mock_auth, registry_auths):
+    """Test successful cache refresh."""
+    tag = 'test-tag'
</code_context>

<issue_to_address>
**suggestion (testing):** Consider testing refresh_indexdb_cache with missing or invalid registry_auths.

Please add tests for cases where registry_auths is None or contains invalid data to verify correct handling by refresh_indexdb_cache.

```suggestion
@mock.patch('iib.workers.tasks.oras_utils.set_registry_auths')
@mock.patch('iib.workers.tasks.oras_utils.run_cmd')
def test_refresh_indexdb_cache_success(mock_run_cmd, mock_auth, registry_auths):
    """Test successful cache refresh."""
    tag = 'test-tag'


@mock.patch('iib.workers.tasks.oras_utils.set_registry_auths')
@mock.patch('iib.workers.tasks.oras_utils.run_cmd')
def test_refresh_indexdb_cache_none_registry_auths(mock_run_cmd, mock_auth):
    """Test refresh_indexdb_cache with registry_auths=None."""
    tag = 'test-tag'
    registry_auths = None
    # Assuming refresh_indexdb_cache raises ValueError or handles None gracefully
    from iib.workers.tasks.oras_utils import refresh_indexdb_cache
    try:
        refresh_indexdb_cache(tag, registry_auths)
    except Exception as exc:
        assert isinstance(exc, (TypeError, ValueError))
    else:
        # If no exception, ensure set_registry_auths is not called
        mock_auth.assert_not_called()


@mock.patch('iib.workers.tasks.oras_utils.set_registry_auths')
@mock.patch('iib.workers.tasks.oras_utils.run_cmd')
def test_refresh_indexdb_cache_invalid_registry_auths(mock_run_cmd, mock_auth):
    """Test refresh_indexdb_cache with invalid registry_auths."""
    tag = 'test-tag'
    # Example of invalid registry_auths: not a dict, missing keys, etc.
    invalid_registry_auths = "not-a-dict"
    from iib.workers.tasks.oras_utils import refresh_indexdb_cache
    try:
        refresh_indexdb_cache(tag, invalid_registry_auths)
    except Exception as exc:
        assert isinstance(exc, (TypeError, ValueError))
    else:
        mock_auth.assert_not_called()
```
</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.

:return: The image digest (e.g., "sha256:...").
:rtype: str
"""
jsonpath = f'\'{{.status.tags[?(@.tag=="{tag}")].items[0].image}}\''
Copy link

Choose a reason for hiding this comment

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

suggestion: The jsonpath string uses single quotes which may cause issues with oc CLI.

Consider removing the single quotes from the jsonpath expression unless they are specifically required, as they may cause parsing issues with run_cmd.

Suggested change
jsonpath = f'\'{{.status.tags[?(@.tag=="{tag}")].items[0].image}}\''
jsonpath = f'{{.status.tags[?(@.tag=="{tag}")].items[0].image}}'

Comment on lines +334 to +335
@mock.patch('iib.workers.tasks.oras_utils.run_cmd')
def test_get_image_stream_digest(mock_run_cmd):
Copy link

Choose a reason for hiding this comment

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

suggestion (testing): Consider testing get_image_stream_digest with unexpected output formats.

Please add tests for cases where run_cmd returns values like an empty string, None, or a non-digest value to verify error handling in get_image_stream_digest.

Suggested implementation:

@mock.patch('iib.workers.tasks.oras_utils.run_cmd')
def test_get_image_stream_digest(mock_run_cmd):
    """Test successful retrieval of image digest from ImageStream."""
    mock_run_cmd.return_value = 'sha256:12345'
    tag = 'test-tag'

    digest = get_image_stream_digest(tag)

    assert digest == 'sha256:12345'
    mock_run_cmd.assert_called_once_with(
        [
            'oc',
            'get',
            'imagestream',
        ]
    )

@mock.patch('iib.workers.tasks.oras_utils.run_cmd')
def test_get_image_stream_digest_empty_string(mock_run_cmd):
    """Test get_image_stream_digest with empty string output."""
    mock_run_cmd.return_value = ''
    tag = 'test-tag'
    try:
        digest = get_image_stream_digest(tag)
    except Exception as exc:
        assert isinstance(exc, Exception)
    else:
        assert digest is None or digest == '', "Expected None or empty digest for empty string output"

@mock.patch('iib.workers.tasks.oras_utils.run_cmd')
def test_get_image_stream_digest_none(mock_run_cmd):
    """Test get_image_stream_digest with None output."""
    mock_run_cmd.return_value = None
    tag = 'test-tag'
    try:
        digest = get_image_stream_digest(tag)
    except Exception as exc:
        assert isinstance(exc, Exception)
    else:
        assert digest is None, "Expected None digest for None output"

@mock.patch('iib.workers.tasks.oras_utils.run_cmd')
def test_get_image_stream_digest_invalid_format(mock_run_cmd):
    """Test get_image_stream_digest with non-digest output."""
    mock_run_cmd.return_value = 'not-a-digest'
    tag = 'test-tag'
    try:
        digest = get_image_stream_digest(tag)
    except Exception as exc:
        assert isinstance(exc, Exception)
    else:
        assert digest is None or digest == 'not-a-digest', "Expected None or raw output for invalid digest format"

You may need to adjust the assertions in the new tests depending on how get_image_stream_digest is implemented (e.g., whether it raises exceptions or returns None for invalid output). If it raises a specific exception, replace Exception with the specific exception type.

Comment on lines +363 to +365
@mock.patch('iib.workers.tasks.oras_utils.get_image_stream_digest')
@mock.patch('iib.workers.tasks.oras_utils.get_image_digest')
def test_verify_indexdb_cache_sync_match(mock_get_image_digest, mock_get_is_digest):
Copy link

Choose a reason for hiding this comment

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

suggestion (testing): Consider adding a test for verify_indexdb_cache_sync when get_image_digest or get_image_stream_digest raises an exception.

Please add a test to verify that verify_indexdb_cache_sync properly handles exceptions from get_image_digest or get_image_stream_digest.

Comment on lines +393 to +397
@mock.patch('iib.workers.tasks.oras_utils.set_registry_auths')
@mock.patch('iib.workers.tasks.oras_utils.run_cmd')
def test_refresh_indexdb_cache_success(mock_run_cmd, mock_auth, registry_auths):
"""Test successful cache refresh."""
tag = 'test-tag'
Copy link

Choose a reason for hiding this comment

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

suggestion (testing): Consider testing refresh_indexdb_cache with missing or invalid registry_auths.

Please add tests for cases where registry_auths is None or contains invalid data to verify correct handling by refresh_indexdb_cache.

Suggested change
@mock.patch('iib.workers.tasks.oras_utils.set_registry_auths')
@mock.patch('iib.workers.tasks.oras_utils.run_cmd')
def test_refresh_indexdb_cache_success(mock_run_cmd, mock_auth, registry_auths):
"""Test successful cache refresh."""
tag = 'test-tag'
@mock.patch('iib.workers.tasks.oras_utils.set_registry_auths')
@mock.patch('iib.workers.tasks.oras_utils.run_cmd')
def test_refresh_indexdb_cache_success(mock_run_cmd, mock_auth, registry_auths):
"""Test successful cache refresh."""
tag = 'test-tag'
@mock.patch('iib.workers.tasks.oras_utils.set_registry_auths')
@mock.patch('iib.workers.tasks.oras_utils.run_cmd')
def test_refresh_indexdb_cache_none_registry_auths(mock_run_cmd, mock_auth):
"""Test refresh_indexdb_cache with registry_auths=None."""
tag = 'test-tag'
registry_auths = None
# Assuming refresh_indexdb_cache raises ValueError or handles None gracefully
from iib.workers.tasks.oras_utils import refresh_indexdb_cache
try:
refresh_indexdb_cache(tag, registry_auths)
except Exception as exc:
assert isinstance(exc, (TypeError, ValueError))
else:
# If no exception, ensure set_registry_auths is not called
mock_auth.assert_not_called()
@mock.patch('iib.workers.tasks.oras_utils.set_registry_auths')
@mock.patch('iib.workers.tasks.oras_utils.run_cmd')
def test_refresh_indexdb_cache_invalid_registry_auths(mock_run_cmd, mock_auth):
"""Test refresh_indexdb_cache with invalid registry_auths."""
tag = 'test-tag'
# Example of invalid registry_auths: not a dict, missing keys, etc.
invalid_registry_auths = "not-a-dict"
from iib.workers.tasks.oras_utils import refresh_indexdb_cache
try:
refresh_indexdb_cache(tag, invalid_registry_auths)
except Exception as exc:
assert isinstance(exc, (TypeError, ValueError))
else:
mock_auth.assert_not_called()

:return: The image digest (e.g., "sha256:...").
:rtype: str
"""
jsonpath = f'\'{{.status.tags[?(@.tag=="{tag}")].items[0].image}}\''
Copy link
Collaborator

Choose a reason for hiding this comment

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

Might be worth adding a comment here for posterity

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.

2 participants