Skip to content

Commit 478a687

Browse files
openshift-cherrypick-robotZaperexrm3l
authored
[release-1.9] chore: fix oci disable bug in install-dynamic-plugins.py (#4654)
* chore: fix oci disable bug in install-dynamic-plugins.py Assisted-By: Cursor Signed-off-by: Frank Kong <frkong@redhat.com> rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED * chore: add warning for disabled entries instead of failing Signed-off-by: Frank Kong <frkong@redhat.com> rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED * chore: fix invalid disabled oci package handling Signed-off-by: Frank Kong <frkong@redhat.com> rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED --------- Co-authored-by: Frank Kong <frkong@redhat.com> Co-authored-by: Armel Soro <asoro@redhat.com>
1 parent 9d8721a commit 478a687

2 files changed

Lines changed: 517 additions & 3 deletions

File tree

docker/install-dynamic-plugins.py

Lines changed: 147 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,6 +1135,136 @@ def extract_catalog_index(catalog_index_image: str, catalog_index_mount: str, ca
11351135

11361136
return default_plugins_file
11371137

1138+
def pre_merge_oci_disabled_state(
1139+
include_plugin_lists: list[tuple[str, list[dict]]],
1140+
main_plugins: list[dict],
1141+
dynamic_plugins_file: str
1142+
) -> set[str]:
1143+
"""
1144+
Pre-merge pass: determine which OCI registries will be disabled after all levels are applied.
1145+
Only considers 'package' and 'disabled' fields. Does NOT merge pluginConfig.
1146+
1147+
Args:
1148+
include_plugin_lists: list of (filename, plugin_list) tuples from each included config file
1149+
main_plugins: plugin list from the main config file
1150+
dynamic_plugins_file: path to the main config file (for error messages)
1151+
Returns:
1152+
Set of OCI registry strings that should skip metadata fetch (final state = disabled)
1153+
"""
1154+
1155+
# Track the disabled state and level of each OCI plugin entry
1156+
per_entry_state = {}
1157+
# Track the source file of each path-less OCI plugin entry
1158+
pathless_plugin_registries = {} # {registry: source_file}
1159+
# Track the source file of each OCI plugin entry with a defined path
1160+
defined_paths_per_plugin_registry = {} # {registry: {path: source_file}}
1161+
1162+
def process_entry(plugin, level, source_file):
1163+
package = plugin.get('package', '')
1164+
if not isinstance(package, str) or not package.startswith(OCI_PROTOCOL_PREFIX):
1165+
return
1166+
disabled = plugin.get('disabled', False)
1167+
match = re.match(OciPackageMerger.EXPECTED_OCI_PATTERN, package)
1168+
if not match:
1169+
if disabled:
1170+
print(f"WARNING: Skipping disabled OCI plugin with invalid format: \'{package}\' in {source_file}. Expected format: \'{OCI_PROTOCOL_PREFIX}<registry>:<tag>\' or \'{OCI_PROTOCOL_PREFIX}<registry>@<algo>:<digest>\' (optionally followed by \'!<path>\') where <registry> may include a port (e.g. host:5000/path) and <algo> is one of {RECOGNIZED_ALGORITHMS}", flush=True)
1171+
return
1172+
raise InstallException(f"oci package \'{package}\' is not in the expected format \'{OCI_PROTOCOL_PREFIX}<registry>:<tag>\' or \'{OCI_PROTOCOL_PREFIX}<registry>@<algo>:<digest>\' (optionally followed by \'!<path>\') in {source_file} where <registry> may include a port (e.g. host:5000/path) and <algo> is one of {RECOGNIZED_ALGORITHMS}")
1173+
registry = match.group(1)
1174+
path = match.group(4) # None for path-less
1175+
1176+
entry_key = (registry, path)
1177+
if entry_key not in per_entry_state:
1178+
per_entry_state[entry_key] = (disabled, level)
1179+
elif per_entry_state[entry_key][1] == level:
1180+
path_suffix = f"!{path}" if path else ""
1181+
if disabled:
1182+
print(f"WARNING: Skipping duplicate disabled OCI plugin configuration for {registry}{path_suffix} in {source_file}", flush=True)
1183+
return
1184+
1185+
raise InstallException(
1186+
f"Duplicate OCI plugin configuration for {registry}{path_suffix} "
1187+
f"found at the same level in {source_file}: {package}"
1188+
)
1189+
elif level > per_entry_state[entry_key][1]:
1190+
per_entry_state[entry_key] = (disabled, level)
1191+
1192+
if path:
1193+
defined_paths_per_plugin_registry.setdefault(registry, {})[path] = source_file
1194+
else:
1195+
pathless_plugin_registries[registry] = source_file
1196+
1197+
for include_file, include_plugins in include_plugin_lists:
1198+
for plugin in include_plugins:
1199+
process_entry(plugin, level=0, source_file=include_file)
1200+
1201+
for plugin in main_plugins:
1202+
process_entry(plugin, level=1, source_file=dynamic_plugins_file)
1203+
1204+
# Initial validation for no multi-plugin OCI packages being defined without explicit path
1205+
for registry, pathless_source in pathless_plugin_registries.items():
1206+
if registry in defined_paths_per_plugin_registry and len(defined_paths_per_plugin_registry[registry]) > 1:
1207+
path_entries = defined_paths_per_plugin_registry[registry]
1208+
paths_formatted = '\n - '.join(
1209+
f"{path} (in {src})" for path, src in sorted(path_entries.items())
1210+
)
1211+
pathless_disabled, _ = per_entry_state[(registry, None)]
1212+
if pathless_disabled:
1213+
print(
1214+
f"WARNING: Skipping disabled ambiguous path-less OCI reference for {registry} in {pathless_source}: "
1215+
f"multiple path-specific entries exist:\n - {paths_formatted}\n"
1216+
f"Cannot use path-less syntax for multi-plugin images. "
1217+
f"Please specify a !<plugin-path> suffix for the plugin",
1218+
flush=True
1219+
)
1220+
continue
1221+
raise InstallException(
1222+
f"Ambiguous path-less OCI reference for {registry} in {pathless_source}: "
1223+
f"multiple path-specific entries exist:\n - {paths_formatted}\n"
1224+
f"Cannot use path-less syntax for multi-plugin images. "
1225+
f"Please specify a !<plugin-path> suffix for the plugin."
1226+
)
1227+
1228+
# Determine effective disabled state for each registry without explicit path.
1229+
disabled_plugin_registries = set()
1230+
for registry in pathless_plugin_registries:
1231+
pathless_disabled, pathless_level = per_entry_state[(registry, None)]
1232+
1233+
effective_disabled = pathless_disabled
1234+
1235+
# Check if a single entry with explicit path at a higher level overrides
1236+
if registry in defined_paths_per_plugin_registry:
1237+
single_path = next(iter(defined_paths_per_plugin_registry[registry]))
1238+
defined_disabled, defined_level = per_entry_state[(registry, single_path)]
1239+
if defined_level > pathless_level:
1240+
effective_disabled = defined_disabled
1241+
1242+
if effective_disabled:
1243+
disabled_plugin_registries.add(registry)
1244+
1245+
return disabled_plugin_registries
1246+
1247+
1248+
def filter_disabled_oci_plugins(plugins: list[dict], disabled_plugin_registries: set[str]) -> list[dict]:
1249+
"""
1250+
Remove all OCI plugin entries whose registry is in the disabled set.
1251+
Non-OCI entries are passed through unchanged.
1252+
"""
1253+
filtered = []
1254+
for plugin in plugins:
1255+
package = plugin.get('package', '')
1256+
if isinstance(package, str) and package.startswith(OCI_PROTOCOL_PREFIX):
1257+
match = re.match(OciPackageMerger.EXPECTED_OCI_PATTERN, package)
1258+
if match and match.group(1) in disabled_plugin_registries:
1259+
print(f'\n======= Disabling OCI plugin {package}', flush=True)
1260+
continue
1261+
if not match and plugin.get('disabled', False):
1262+
print(f'\n======= Disabling OCI plugin {package}', flush=True)
1263+
continue
1264+
filtered.append(plugin)
1265+
return filtered
1266+
1267+
11381268
def main():
11391269

11401270
dynamic_plugins_root = sys.argv[1]
@@ -1207,6 +1337,7 @@ def main():
12071337
index = includes.index(embedded_default)
12081338
includes[index] = catalog_index_default_file
12091339

1340+
include_plugin_lists = [] # [(filename, plugin_list), ...]
12101341
for include in includes:
12111342
if not isinstance(include, str):
12121343
raise InstallException(f"content of the \'includes\' field must be a list of strings in {dynamic_plugins_file}")
@@ -1227,8 +1358,7 @@ def main():
12271358
if not isinstance(include_plugins, list):
12281359
raise InstallException(f"content of the \'plugins\' field must be a list in {include}")
12291360

1230-
for plugin in include_plugins:
1231-
merge_plugin(plugin, all_plugins, include, level=0)
1361+
include_plugin_lists.append((include, include_plugins))
12321362

12331363
if 'plugins' in content:
12341364
plugins = content['plugins']
@@ -1238,6 +1368,21 @@ def main():
12381368
if not isinstance(plugins, list):
12391369
raise InstallException(f"content of the \'plugins\' field must be a list in {dynamic_plugins_file}")
12401370

1371+
# Pre-merge: determine disabled OCI registries before any skopeo calls
1372+
disabled_plugin_registries = pre_merge_oci_disabled_state(
1373+
include_plugin_lists, plugins, dynamic_plugins_file
1374+
)
1375+
1376+
# Filter disabled OCI entries from all plugin lists before resolving paths and merging
1377+
for i, (include_file, include_plugins) in enumerate(include_plugin_lists):
1378+
include_plugin_lists[i] = (include_file, filter_disabled_oci_plugins(include_plugins, disabled_plugin_registries))
1379+
1380+
plugins = filter_disabled_oci_plugins(plugins, disabled_plugin_registries)
1381+
1382+
for include_file, include_plugins in include_plugin_lists:
1383+
for plugin in include_plugins:
1384+
merge_plugin(plugin, all_plugins, include_file, level=0)
1385+
12411386
for plugin in plugins:
12421387
merge_plugin(plugin, all_plugins, dynamic_plugins_file, level=1)
12431388

0 commit comments

Comments
 (0)