@@ -2312,7 +2312,7 @@ def mock_oci_image(self, tmp_path):
23122312"""
23132313 yaml_file .write_text (yaml_content )
23142314
2315- # Create catalog entities directory structure
2315+ # Create catalog entities directory structure (using marketplace for backward compatibility)
23162316 catalog_entities_dir = layer_content_dir / "catalog-entities" / "marketplace"
23172317 catalog_entities_dir .mkdir (parents = True )
23182318 entity_file = catalog_entities_dir / "test-entity.yaml"
@@ -2334,6 +2334,67 @@ def mock_oci_image(self, tmp_path):
23342334 "entity_file" : str (entity_file )
23352335 }
23362336
2337+ @pytest .fixture
2338+ def mock_oci_image_with_extensions (self , tmp_path ):
2339+ """Create a mock OCI image structure with extensions directory (new format)."""
2340+ import tarfile
2341+
2342+ # Create a temporary directory for the OCI image
2343+ oci_dir = tmp_path / "oci-image-extensions"
2344+ oci_dir .mkdir ()
2345+
2346+ # Create manifest.json
2347+ manifest = {
2348+ "schemaVersion" : 2 ,
2349+ "mediaType" : "application/vnd.oci.image.manifest.v1+json" ,
2350+ "config" : {
2351+ "mediaType" : "application/vnd.oci.image.config.v1+json" ,
2352+ "digest" : "sha256:test456" ,
2353+ "size" : 100
2354+ },
2355+ "layers" : [
2356+ {
2357+ "mediaType" : "application/vnd.oci.image.layer.v1.tar+gzip" ,
2358+ "digest" : "sha256:def789ghi012" ,
2359+ "size" : 1000
2360+ }
2361+ ]
2362+ }
2363+ manifest_path = oci_dir / "manifest.json"
2364+ manifest_path .write_text (json .dumps (manifest ))
2365+
2366+ # Create a layer tarball with dynamic-plugins.default.yaml and catalog entities
2367+ layer_content_dir = tmp_path / "layer-content-extensions"
2368+ layer_content_dir .mkdir ()
2369+
2370+ yaml_file = layer_content_dir / "dynamic-plugins.default.yaml"
2371+ yaml_content = """plugins:
2372+ - package: '@backstage/plugin-catalog'
2373+ integrity: sha512-test
2374+ """
2375+ yaml_file .write_text (yaml_content )
2376+
2377+ # Create catalog entities directory structure using extensions (new format)
2378+ catalog_entities_dir = layer_content_dir / "catalog-entities" / "extensions"
2379+ catalog_entities_dir .mkdir (parents = True )
2380+ entity_file = catalog_entities_dir / "test-entity.yaml"
2381+ entity_file .write_text ("apiVersion: backstage.io/v1alpha1\n kind: Component\n metadata:\n name: test-extensions" )
2382+
2383+ # Create the layer tarball
2384+ layer_tarball = oci_dir / "def789ghi012"
2385+ with create_test_tarball (layer_tarball ) as tar :
2386+ tar .add (str (yaml_file ), arcname = "dynamic-plugins.default.yaml" )
2387+ # Add catalog entities directory structure recursively
2388+ tar .add (str (layer_content_dir / "catalog-entities" ), arcname = "catalog-entities" , recursive = True )
2389+
2390+ return {
2391+ "oci_dir" : str (oci_dir ),
2392+ "manifest_path" : str (manifest_path ),
2393+ "layer_tarball" : str (layer_tarball ),
2394+ "yaml_content" : yaml_content ,
2395+ "entity_file" : str (entity_file )
2396+ }
2397+
23372398 def test_extract_catalog_index_skopeo_not_found (self , tmp_path , mocker ):
23382399 """Test that function raises InstallException when skopeo is not available."""
23392400 mocker .patch ('shutil.which' , return_value = None )
@@ -2675,8 +2736,87 @@ def test_extract_catalog_index_removes_existing_destination(self, tmp_path, mock
26752736 assert not (entities_dir / "old-file.yaml" ).exists (), "Old file should not exist"
26762737 assert not (entities_dir / "old-subdir" ).exists (), "Old subdirectory should not exist"
26772738
2739+ def test_extract_catalog_index_uses_extensions_directory (self , tmp_path , mocker , mock_oci_image_with_extensions , capsys ):
2740+ """Test that extraction prefers extensions directory over marketplace."""
2741+ catalog_mount = tmp_path / "catalog-mount"
2742+ catalog_mount .mkdir ()
2743+ catalog_entities_parent_dir = tmp_path / "entities-extensions"
2744+
2745+ mocker .patch ('shutil.which' , return_value = '/usr/bin/skopeo' )
2746+
2747+ mock_result = mocker .Mock ()
2748+ mock_result .returncode = 0
2749+ mock_subprocess_run = create_mock_skopeo_copy (
2750+ mock_oci_image_with_extensions ['manifest_path' ],
2751+ mock_oci_image_with_extensions ['layer_tarball' ],
2752+ mock_result
2753+ )
2754+ mocker .patch ('subprocess.run' , side_effect = mock_subprocess_run )
2755+
2756+ result = install_dynamic_plugins .extract_catalog_index (
2757+ "quay.io/test/catalog-index-extensions:1.9" ,
2758+ str (catalog_mount ),
2759+ str (catalog_entities_parent_dir )
2760+ )
2761+
2762+ # Verify the function returned a path
2763+ assert result is not None
2764+ assert result .endswith ('dynamic-plugins.default.yaml' )
2765+
2766+ # Verify catalog entities were extracted from extensions directory
2767+ entities_dir = catalog_entities_parent_dir / "catalog-entities"
2768+ assert entities_dir .exists ()
2769+ entity_file = entities_dir / "test-entity.yaml"
2770+ assert entity_file .exists ()
2771+ assert "kind: Component" in entity_file .read_text ()
2772+ assert "test-extensions" in entity_file .read_text ()
2773+
2774+ # Verify success messages were printed
2775+ captured = capsys .readouterr ()
2776+ assert 'Successfully extracted dynamic-plugins.default.yaml' in captured .out
2777+ assert 'Successfully extracted extensions catalog entities' in captured .out
2778+
2779+ def test_extract_catalog_index_falls_back_to_marketplace (self , tmp_path , mocker , mock_oci_image , capsys ):
2780+ """Test that extraction falls back to marketplace directory when extensions doesn't exist."""
2781+ catalog_mount = tmp_path / "catalog-mount"
2782+ catalog_mount .mkdir ()
2783+ catalog_entities_parent_dir = tmp_path / "entities-marketplace"
2784+
2785+ mocker .patch ('shutil.which' , return_value = '/usr/bin/skopeo' )
2786+
2787+ mock_result = mocker .Mock ()
2788+ mock_result .returncode = 0
2789+ mock_subprocess_run = create_mock_skopeo_copy (
2790+ mock_oci_image ['manifest_path' ],
2791+ mock_oci_image ['layer_tarball' ],
2792+ mock_result
2793+ )
2794+ mocker .patch ('subprocess.run' , side_effect = mock_subprocess_run )
2795+
2796+ result = install_dynamic_plugins .extract_catalog_index (
2797+ "quay.io/test/catalog-index-marketplace:1.9" ,
2798+ str (catalog_mount ),
2799+ str (catalog_entities_parent_dir )
2800+ )
2801+
2802+ # Verify the function returned a path
2803+ assert result is not None
2804+ assert result .endswith ('dynamic-plugins.default.yaml' )
2805+
2806+ # Verify catalog entities were extracted from marketplace directory (fallback)
2807+ entities_dir = catalog_entities_parent_dir / "catalog-entities"
2808+ assert entities_dir .exists ()
2809+ entity_file = entities_dir / "test-entity.yaml"
2810+ assert entity_file .exists ()
2811+ assert "kind: Component" in entity_file .read_text ()
2812+
2813+ # Verify success messages were printed
2814+ captured = capsys .readouterr ()
2815+ assert 'Successfully extracted dynamic-plugins.default.yaml' in captured .out
2816+ assert 'Successfully extracted extensions catalog entities' in captured .out
2817+
26782818 def test_extract_catalog_index_without_catalog_entities (self , tmp_path , mocker , capsys ):
2679- """Test that extraction succeeds with warning if catalog-entities directory doesn't exist in image ."""
2819+ """Test that extraction succeeds with warning if neither extensions nor marketplace directory exists ."""
26802820 import tarfile
26812821
26822822 catalog_mount = tmp_path / "catalog-mount"
@@ -2727,11 +2867,12 @@ def test_extract_catalog_index_without_catalog_entities(self, tmp_path, mocker,
27272867 assert result is not None
27282868 assert result .endswith ('dynamic-plugins.default.yaml' )
27292869
2730- # Verify warning was printed
2870+ # Verify warning was printed with both directory names
27312871 captured = capsys .readouterr ()
27322872 assert 'WARNING' in captured .out
2733- assert 'does not have a' in captured .out
2734- assert 'catalog-entities/marketplace' in captured .out
2873+ assert 'does not have neither' in captured .out
2874+ assert 'catalog-entities/extensions/' in captured .out
2875+ assert 'catalog-entities/marketplace/' in captured .out
27352876
27362877 # Verify catalog entities directory was not created
27372878 entities_dir = catalog_entities_parent_dir / "catalog-entities"
0 commit comments