Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
7 changes: 7 additions & 0 deletions doozer/doozerlib/backend/konflux_fbc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1223,6 +1223,13 @@ def _update_channel(channel: Dict):
# and add the bundle name of bundle_with_skips to the skips field
skips = set(bundle_with_skips.pop('skips'))
skips = (skips | {bundle_with_skips['name']}) - {olm_bundle_name}
# Also collect skips from any remaining entries (handles branching
# upgrade graphs where multiple entries independently define skips)
for other in channel['entries']:
if other is not bundle_with_skips and other.get('skips'):
skips.update(other.pop('skips'))
skips.add(other['name'])
skips.discard(olm_bundle_name)
elif len(channel['entries']) == 1:
# The channel only has one entry like
# ------------
Expand Down
191 changes: 191 additions & 0 deletions doozer/tests/backend/test_konflux_fbc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1932,6 +1932,197 @@ async def test_rebase_dir_writes_skips_for_openshift_when_disabled(
self.assertIn("skips", new_entry, "OpenShift groups must still write skips even when flag is True")
self.assertEqual(new_entry["skipRange"], ">=6.3.0-0 <6.5.1")

@patch("doozerlib.opm.generate_dockerfile")
@patch("pathlib.Path.unlink")
@patch("doozerlib.backend.konflux_fbc.KonfluxFbcRebaser._load_csv_from_bundle")
@patch("doozerlib.backend.konflux_fbc.KonfluxFbcRebaser._get_referenced_images")
@patch("doozerlib.backend.konflux_fbc.DockerfileParser")
@patch("pathlib.Path.mkdir")
@patch("pathlib.Path.is_file", return_value=True)
@patch("pathlib.Path.open")
@patch("doozerlib.backend.konflux_fbc.KonfluxFbcRebaser._fetch_olm_bundle_image_info", new_callable=AsyncMock)
@patch("doozerlib.backend.konflux_fbc.KonfluxFbcRebaser._fetch_olm_bundle_blob", new_callable=AsyncMock)
async def test_rebase_dir_branching_skips_graph(
self,
mock_fetch_olm_bundle_blob,
mock_fetch_olm_bundle_image_info,
mock_open,
mock_is_file,
mock_mkdir,
MockDockerfileParser,
mock_get_referenced_images,
mock_load_csv_from_bundle: AsyncMock,
mock_path_unlink: Mock,
mock_generate_dockerfile: AsyncMock,
):
"""Multiple entries with independent skips (branching graph like ACM) are all collected."""
rebaser = KonfluxFbcRebaser(
base_dir=self.base_dir,
group="acm-2.16",
assembly=self.assembly,
version="2.16.3",
release="1",
commit_message="test",
push=False,
fbc_repo="https://example.com/fbc.git",
upcycle=False,
)

metadata = MagicMock(spec=ImageMetadata)
metadata.distgit_key = "multiclusterhub-operator"
metadata.runtime = MagicMock()
metadata.runtime.group_config = MagicMock()
metadata.runtime.group_config.vars = MagicMock()
metadata.runtime.group_config.vars.MAJOR = "2"
metadata.runtime.group_config.vars.MINOR = "16"
metadata.runtime.group_config.vars.FBC_DISABLE_CHANNEL_SKIPS = False
metadata.runtime.konflux_db = MagicMock()
metadata.config = MagicMock()
metadata.config.delivery = MagicMock()
metadata.config.delivery.delivery_repo_names = ["acm-hub-repo"]
metadata.get_olm_bundle_delivery_repo_name.return_value = "acm-hub-repo"

build_repo = MagicMock()
build_repo.local_dir = self.base_dir
bundle_build = MagicMock(
spec=KonfluxBundleBuildRecord,
nvr="multiclusterhub-operator-bundle-2.16.3-1",
operator_nvr="multiclusterhub-operator-2.16.3-1",
operand_nvrs=[],
image_pullspec="example.com/acm-bundle@sha256:abc",
image_tag="abc123",
source_repo="https://example.com/multiclusterhub-operator.git",
commitish="abc123def",
)
logger = MagicMock()

mock_fetch_olm_bundle_image_info.return_value = {
"config": {
"config": {
"Labels": {
"name": "multiclusterhub-operator",
"operators.operatorframework.io.bundle.channels.v1": "release-2.16",
"operators.operatorframework.io.bundle.channel.default.v1": "release-2.16",
"operators.operatorframework.io.bundle.package.v1": "advanced-cluster-management",
"operators.operatorframework.io.bundle.manifests.v1": "manifests/",
},
},
},
}

mock_fetch_olm_bundle_blob.return_value = (
"advanced-cluster-management.v2.16.3",
"advanced-cluster-management",
{
"schema": "olm.bundle",
"name": "advanced-cluster-management.v2.16.3",
"package": "advanced-cluster-management",
"properties": [],
"relatedImages": [{"name": "", "image": "example.com/acm-bundle@sha256:abc"}],
},
)

# Branching upgrade graph: v2.15.3 skips v2.14.5, v2.16.0 skips v2.15.3.
# Both entries independently have skips — doozer must collect from ALL of them.
org_catalog_blobs = [
{"schema": "olm.package", "name": "advanced-cluster-management", "defaultChannel": "release-2.16"},
{
"schema": "olm.channel",
"name": "release-2.16",
"package": "advanced-cluster-management",
"entries": [
{"name": "advanced-cluster-management.v2.14.5", "skipRange": ">=2.12.0 <2.14.5"},
{
"name": "advanced-cluster-management.v2.15.3",
"skipRange": ">=2.12.0 <2.15.3",
"skips": ["advanced-cluster-management.v2.14.5"],
},
{
"name": "advanced-cluster-management.v2.16.0",
"skipRange": ">=2.12.0 <2.16.0",
"skips": ["advanced-cluster-management.v2.15.3"],
},
],
},
{
"schema": "olm.bundle",
"name": "advanced-cluster-management.v2.14.5",
"package": "advanced-cluster-management",
"properties": [],
"relatedImages": [],
},
{
"schema": "olm.bundle",
"name": "advanced-cluster-management.v2.15.3",
"package": "advanced-cluster-management",
"properties": [],
"relatedImages": [],
},
{
"schema": "olm.bundle",
"name": "advanced-cluster-management.v2.16.0",
"package": "advanced-cluster-management",
"properties": [],
"relatedImages": [],
},
]

org_catalog_file = StringIO()
yaml.dump_all(org_catalog_blobs, org_catalog_file)
org_catalog_file.seek(0)
result_catalog_file = StringIO()
images_mirror_set_file = StringIO()
mock_open.return_value.__enter__.side_effect = [org_catalog_file, result_catalog_file, images_mirror_set_file]

mock_get_referenced_images.return_value = []
mock_load_csv_from_bundle.return_value = {
"metadata": {
"name": "multiclusterhub-operator",
"annotations": {"olm.skipRange": ">=2.12.0 <2.16.3"},
},
"spec": {"icon": []},
}

mock_dfp = MockDockerfileParser.return_value
mock_dfp.envs = {}
mock_dfp.labels = {}

mock_generate_dockerfile.return_value = None

await rebaser._rebase_dir(metadata, build_repo, bundle_build, "2.16.3", "1", logger)

result_catalog_file.seek(0)
result_catalog_blobs = list(yaml.load_all(result_catalog_file))
result_catalog_blobs = rebaser._catagorize_catalog_blobs(result_catalog_blobs)

entries = result_catalog_blobs["advanced-cluster-management"]["olm.channel"]["release-2.16"]["entries"]

# The new entry must skip ALL previous versions (union of all skip chains)
new_entry = next(e for e in entries if e["name"] == "advanced-cluster-management.v2.16.3")
self.assertIn("skips", new_entry)
self.assertEqual(
sorted(new_entry["skips"]),
sorted(
[
"advanced-cluster-management.v2.14.5",
"advanced-cluster-management.v2.15.3",
"advanced-cluster-management.v2.16.0",
]
),
)

# Previous entries must have their skips removed (new head owns the skip chain)
for entry in entries:
if entry["name"] != "advanced-cluster-management.v2.16.3":
self.assertNotIn(
"skips",
entry,
f"Entry {entry['name']} should not retain skips after new head is added",
)

# replaces is set to the first unreplaced entry (existing behavior for non-openshift groups)
self.assertIn("replaces", new_entry)
Comment thread
coderabbitai[bot] marked this conversation as resolved.


class TestFetchCsvFromGit(unittest.IsolatedAsyncioTestCase):
"""Test the _fetch_csv_from_git helper function."""
Expand Down