@@ -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+
11381268def 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