Skip to content

Commit e054ac7

Browse files
Merge pull request #2563 from vfreex/feat-fbc-add-missing-entry-clean
Add --insert-missing-entry flag for version-ordered FBC catalog entries
2 parents 5455a44 + 55177ea commit e054ac7

File tree

4 files changed

+482
-15
lines changed

4 files changed

+482
-15
lines changed

doozer/doozerlib/backend/konflux_fbc.py

Lines changed: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from doozerlib.image import ImageMetadata
3737
from doozerlib.record_logger import RecordLogger
3838
from elliottlib.shipment_utils import get_shipment_config_from_mr
39+
from semver import VersionInfo
3940
from tenacity import retry, stop_after_attempt, wait_fixed
4041

4142

@@ -655,6 +656,7 @@ def __init__(
655656
fbc_repo: str,
656657
upcycle: bool,
657658
ocp_version_override: Optional[Tuple[int, int]] = None,
659+
insert_missing_entry: bool = False,
658660
record_logger: Optional[RecordLogger] = None,
659661
logger: Optional[logging.Logger] = None,
660662
) -> None:
@@ -670,11 +672,60 @@ def __init__(
670672
self.ocp_version_override = ocp_version_override
671673
self._record_logger = record_logger
672674
self._logger = logger or LOGGER.getChild(self.__class__.__name__)
675+
self.insert_missing_entry = insert_missing_entry
673676

674677
@staticmethod
675678
def get_fbc_name(image_name: str):
676679
return f"{image_name}-fbc"
677680

681+
@staticmethod
682+
def _extract_version_from_bundle_name(bundle_name: str) -> VersionInfo:
683+
"""Extract semantic version from bundle name.
684+
685+
Bundle names follow format: {operator-name}.v{major}.{minor}.{patch}
686+
Example: "oadp-operator.v1.3.9" -> VersionInfo(1, 3, 9)
687+
688+
:param bundle_name: The bundle name
689+
:return: VersionInfo object
690+
:raises ValueError: If version cannot be parsed from bundle name
691+
"""
692+
# Find the version part (starts with .v followed by semantic version)
693+
# Split by '.' and look for the part starting with 'v'
694+
parts = bundle_name.split('.')
695+
for i, part in enumerate(parts):
696+
if part.startswith('v') and len(parts) > i + 2:
697+
# Extract vX.Y.Z format
698+
version_str = '.'.join([part[1:]] + parts[i + 1 : i + 3]) # Remove 'v' prefix
699+
try:
700+
return VersionInfo.parse(version_str)
701+
except Exception as e:
702+
raise ValueError(
703+
f"Cannot parse semantic version from bundle name '{bundle_name}'. "
704+
f"Expected format: {{operator-name}}.v{{major}}.{{minor}}.{{patch}}"
705+
) from e
706+
# If we reach here, no version was found
707+
raise ValueError(
708+
f"Cannot parse semantic version from bundle name '{bundle_name}'. "
709+
f"Expected format: {{operator-name}}.v{{major}}.{{minor}}.{{patch}}"
710+
)
711+
712+
@staticmethod
713+
def _rebuild_replaces_chain(entries: list) -> None:
714+
"""Rebuild replaces field for all entries based on their sorted order.
715+
716+
After sorting entries by version, each entry should replace the previous
717+
entry in the list, forming a linear upgrade chain.
718+
719+
:param entries: List of channel entries sorted by version
720+
"""
721+
for i in range(len(entries)):
722+
if i == 0:
723+
# First entry (oldest version) replaces nothing
724+
entries[i].pop('replaces', None)
725+
else:
726+
# Each subsequent entry replaces the previous one
727+
entries[i]['replaces'] = entries[i - 1]['name']
728+
678729
def _find_future_release_assembly(
679730
self,
680731
releases_config,
@@ -1140,24 +1191,41 @@ def _update_channel(channel: Dict):
11401191
assembly_entry["skipRange"] = assembly_csv_info.skip_range
11411192
channel['entries'].append(assembly_entry)
11421193

1143-
# For an operator bundle that uses replaces -- such as OADP
1144-
# Update "replaces" in the channel
1145-
replaces = None
1146-
if not self.group.startswith('openshift-'):
1147-
# Find the current head - the entry that is not replaced by any other entry
1148-
bundle_with_replaces = [it for it in channel['entries']]
1149-
replaced_names = {it.get('replaces') for it in bundle_with_replaces if it.get('replaces')}
1150-
current_head = next((it for it in bundle_with_replaces if it['name'] not in replaced_names), None)
1151-
if current_head:
1152-
# The new bundle should replace the current head
1153-
replaces = current_head['name']
1154-
11551194
# Add the current bundle to the specified channel in the catalog
11561195
entry = next((entry for entry in channel['entries'] if entry['name'] == olm_bundle_name), None)
11571196
if not entry:
11581197
logger.info("Adding bundle %s to channel %s", olm_bundle_name, channel['name'])
11591198
entry = {"name": olm_bundle_name}
1160-
channel['entries'].append(entry)
1199+
1200+
if self.insert_missing_entry:
1201+
# Add entry and then sort all entries in ascending version order
1202+
channel['entries'].append(entry)
1203+
1204+
logger.info("Sorting channel entries by version order")
1205+
channel['entries'].sort(key=lambda e: self._extract_version_from_bundle_name(e['name']))
1206+
1207+
# Rebuild replaces chain for non-OpenShift groups (operators using replaces)
1208+
if not self.group.startswith('openshift-'):
1209+
logger.info("Rebuilding replaces chain after version sorting")
1210+
self._rebuild_replaces_chain(channel['entries'])
1211+
else:
1212+
# For an operator bundle that uses replaces -- such as OADP
1213+
# Calculate replaces based on current head before appending
1214+
replaces = None
1215+
if not self.group.startswith('openshift-'):
1216+
# Find the current head - the entry that is not replaced by any other entry
1217+
bundle_with_replaces = [it for it in channel['entries']]
1218+
replaced_names = {it.get('replaces') for it in bundle_with_replaces if it.get('replaces')}
1219+
current_head = next(
1220+
(it for it in bundle_with_replaces if it['name'] not in replaced_names), None
1221+
)
1222+
if current_head:
1223+
# The new bundle should replace the current head
1224+
replaces = current_head['name']
1225+
1226+
channel['entries'].append(entry)
1227+
if replaces:
1228+
entry["replaces"] = replaces
11611229
else:
11621230
logger.warning("Bundle %s already exists in channel %s. Replacing...", olm_bundle_name, channel['name'])
11631231
entry.clear()
@@ -1166,8 +1234,6 @@ def _update_channel(channel: Dict):
11661234
entry["skipRange"] = olm_skip_range
11671235
if skips:
11681236
entry["skips"] = sorted(skips)
1169-
if replaces:
1170-
entry["replaces"] = replaces
11711237

11721238
for channel_name in channel_names:
11731239
logger.info("Updating channel %s", channel_name)

doozer/doozerlib/cli/fbc.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,7 @@ def __init__(
461461
reset_to_prod: bool,
462462
prod_registry_auth: Optional[str] = None,
463463
major_minor: Optional[str] = None,
464+
insert_missing_entry: bool = False,
464465
):
465466
self.runtime = runtime
466467
self.version = version
@@ -480,6 +481,7 @@ def __init__(
480481
self.reset_to_prod = reset_to_prod
481482
self.prod_registry_auth = prod_registry_auth
482483
self.major_minor = major_minor
484+
self.insert_missing_entry = insert_missing_entry
483485
self._logger = LOGGER.getChild("FbcRebaseAndBuildCli")
484486
self._db_for_bundles = KonfluxDb()
485487
self._db_for_bundles.bind(KonfluxBundleBuildRecord)
@@ -704,6 +706,7 @@ async def run(self):
704706
fbc_repo=self.fbc_repo,
705707
upcycle=runtime.upcycle,
706708
ocp_version_override=ocp_version if self.major_minor else None,
709+
insert_missing_entry=self.insert_missing_entry,
707710
record_logger=runtime.record_logger,
708711
)
709712

@@ -836,6 +839,12 @@ async def run(self):
836839
metavar='MAJOR.MINOR',
837840
help="Override the MAJOR.MINOR version from group config (e.g. 4.17).",
838841
)
842+
@click.option(
843+
"--insert-missing-entry",
844+
is_flag=True,
845+
default=False,
846+
help="Insert the new bundle entry in version order instead of appending. Use this to fix missing entries that were removed from the catalog.",
847+
)
839848
@click.argument('operator_nvrs', nargs=-1, required=False)
840849
@pass_runtime
841850
@click_coroutine
@@ -857,6 +866,7 @@ async def fbc_rebase_and_build(
857866
reset_to_prod: bool,
858867
prod_registry_auth: Optional[str],
859868
major_minor: Optional[str],
869+
insert_missing_entry: bool,
860870
operator_nvrs: Tuple[str, ...],
861871
):
862872
"""
@@ -896,5 +906,6 @@ async def fbc_rebase_and_build(
896906
reset_to_prod=reset_to_prod,
897907
prod_registry_auth=prod_registry_auth,
898908
major_minor=major_minor,
909+
insert_missing_entry=insert_missing_entry,
899910
)
900911
await cli.run()

0 commit comments

Comments
 (0)