Skip to content

Commit f2b51a6

Browse files
committed
Search for the catalog entities folder in the 'extensions' folder from the catalog index image, with a fallback to 'marketplace' (for backward compatibility)
1 parent 01e7005 commit f2b51a6

File tree

2 files changed

+156
-9
lines changed

2 files changed

+156
-9
lines changed

docker/install-dynamic-plugins.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,17 +1002,23 @@ def extract_catalog_index(catalog_index_image: str, catalog_index_mount: str, ca
10021002
print("\t==> Successfully extracted dynamic-plugins.default.yaml from catalog index image", flush=True)
10031003

10041004
print(f"\t==> Extracting extensions catalog entities to {catalog_entities_parent_dir}", flush=True)
1005-
marketplace_dir_from_catalog_index = os.path.join(catalog_index_temp_dir, 'catalog-entities', 'marketplace')
1006-
if os.path.isdir(marketplace_dir_from_catalog_index):
1005+
1006+
extensions_dir_from_catalog_index = os.path.join(catalog_index_temp_dir, 'catalog-entities', 'extensions')
1007+
if not os.path.isdir(extensions_dir_from_catalog_index):
1008+
# fallback to 'catalog-entities/marketplace' directory for backward compatibility
1009+
extensions_dir_from_catalog_index = os.path.join(catalog_index_temp_dir, 'catalog-entities', 'marketplace')
1010+
1011+
if os.path.isdir(extensions_dir_from_catalog_index):
10071012
os.makedirs(catalog_entities_parent_dir, exist_ok=True)
10081013
catalog_entities_dest = os.path.join(catalog_entities_parent_dir, 'catalog-entities')
10091014
# Ensure the destination directory is is sync with the catalog entities from the index image
10101015
if os.path.exists(catalog_entities_dest):
10111016
shutil.rmtree(catalog_entities_dest, ignore_errors=True, onerror=None)
1012-
shutil.copytree(marketplace_dir_from_catalog_index, catalog_entities_dest, dirs_exist_ok=True)
1017+
shutil.copytree(extensions_dir_from_catalog_index, catalog_entities_dest, dirs_exist_ok=True)
10131018
print("\t==> Successfully extracted extensions catalog entities from index image", flush=True)
10141019
else:
1015-
print(f"\t==> WARNING: Catalog index image {catalog_index_image} does not have a 'catalog-entities/marketplace' directory", flush=True)
1020+
print(f"\t==> WARNING: Catalog index image {catalog_index_image} does not have neither 'catalog-entities/extensions/' nor 'catalog-entities/marketplace/' directory",
1021+
flush=True)
10161022

10171023
return default_plugins_file
10181024

docker/test_install-dynamic-plugins.py

Lines changed: 146 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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\nkind: Component\nmetadata:\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

Comments
 (0)