Skip to content

Commit b557c8f

Browse files
committed
Make package layout in the published repository configurable
closes #3874
1 parent 3f91702 commit b557c8f

File tree

8 files changed

+159
-44
lines changed

8 files changed

+159
-44
lines changed

CHANGES/3874.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Make the layout of the packages in the published repository configurable.

functest_requirements.txt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1+
dictdiffer
12
django # TODO: test_sync.py has a dependency on date parsing functions
3+
lxml
24
productmd>=1.25
3-
pytest<8
4-
dictdiffer
5-
xmltodict
5+
pydantic
66
pyyaml
7-
lxml
8-
pyzstd
9-
requests
7+
pytest<8
108
pytest-xdist
119
pytest-timeout
1210
pytest-custom_exit_code
11+
pyzstd
12+
requests
13+
xmltodict

pulp_rpm/app/constants.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@
1111
(COMPRESSION_TYPES.GZ, COMPRESSION_TYPES.GZ),
1212
)
1313

14+
# publication layout
15+
LAYOUT_TYPES = SimpleNamespace(NESTED_ALPHABETICALLY="nested_alphabetically", FLAT="flat")
16+
17+
LAYOUT_CHOICES = (
18+
(LAYOUT_TYPES.NESTED_ALPHABETICALLY, LAYOUT_TYPES.NESTED_ALPHABETICALLY),
19+
(LAYOUT_TYPES.FLAT, LAYOUT_TYPES.FLAT),
20+
)
21+
1422
CHECKSUM_TYPES = SimpleNamespace(
1523
UNKNOWN="unknown",
1624
MD5="md5",
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 4.2.10 on 2025-02-12 19:58
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('rpm', '0062_rpmpackagesigningservice_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='rpmpublication',
15+
name='layout',
16+
field=models.TextField(choices=[('nested_alphabetically', 'nested_alphabetically'), ('flat', 'flat')], null=True),
17+
),
18+
migrations.AddField(
19+
model_name='rpmrepository',
20+
name='layout',
21+
field=models.TextField(choices=[('nested_alphabetically', 'nested_alphabetically'), ('flat', 'flat')], null=True),
22+
),
23+
]

pulp_rpm/app/models/repository.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
validate_version_paths,
3131
)
3232

33-
from pulp_rpm.app.constants import CHECKSUM_CHOICES, COMPRESSION_CHOICES
33+
from pulp_rpm.app.constants import CHECKSUM_CHOICES, COMPRESSION_CHOICES, LAYOUT_CHOICES
3434
from pulp_rpm.app.downloaders import RpmDownloader, RpmFileDownloader, UlnDownloader
3535
from pulp_rpm.app.exceptions import DistributionTreeConflict
3636
from pulp_rpm.app.models import (
@@ -208,6 +208,8 @@ class RpmRepository(Repository, AutoAddObjPermsMixin):
208208
repo_config (JSON): repo configuration that will be served by distribution
209209
compression_type(pulp_rpm.app.constants.COMPRESSION_TYPES):
210210
Compression type to use for metadata files.
211+
layout(pulp_rpm.app.constants.LAYOUT_TYPES):
212+
How to layout the package files within the publication (flat, nested, etc.)
211213
"""
212214

213215
TYPE = "rpm"
@@ -240,6 +242,7 @@ class RpmRepository(Repository, AutoAddObjPermsMixin):
240242
autopublish = models.BooleanField(default=False)
241243
checksum_type = models.TextField(null=True, choices=CHECKSUM_CHOICES)
242244
compression_type = models.TextField(null=True, choices=COMPRESSION_CHOICES)
245+
layout = models.TextField(null=True, choices=LAYOUT_CHOICES)
243246
metadata_checksum_type = models.TextField(null=True, choices=CHECKSUM_CHOICES)
244247
package_checksum_type = models.TextField(null=True, choices=CHECKSUM_CHOICES)
245248
repo_config = models.JSONField(default=dict)
@@ -267,6 +270,7 @@ def on_new_version(self, version):
267270
},
268271
repo_config=self.repo_config,
269272
compression_type=self.compression_type,
273+
layout=self.layout,
270274
)
271275

272276
@property
@@ -500,6 +504,7 @@ class RpmPublication(Publication, AutoAddObjPermsMixin):
500504
compression_type = models.TextField(null=True, choices=COMPRESSION_CHOICES)
501505
metadata_checksum_type = models.TextField(choices=CHECKSUM_CHOICES)
502506
package_checksum_type = models.TextField(choices=CHECKSUM_CHOICES)
507+
layout = models.TextField(null=True, choices=LAYOUT_CHOICES)
503508
repo_config = models.JSONField(default=dict)
504509

505510
class Meta:

pulp_rpm/app/serializers/repository.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
ALLOWED_PUBLISH_CHECKSUMS,
3030
CHECKSUM_CHOICES,
3131
COMPRESSION_CHOICES,
32+
LAYOUT_CHOICES,
3233
SKIP_TYPES,
3334
SYNC_POLICY_CHOICES,
3435
)
@@ -117,6 +118,12 @@ class RpmRepositorySerializer(RepositorySerializer):
117118
required=False,
118119
allow_null=True,
119120
)
121+
layout = serializers.ChoiceField(
122+
help_text=_("How to layout the packages within the published repository."),
123+
choices=LAYOUT_CHOICES,
124+
required=False,
125+
allow_null=True,
126+
)
120127
gpgcheck = serializers.IntegerField(
121128
max_value=1,
122129
min_value=0,
@@ -278,6 +285,7 @@ class Meta:
278285
"sqlite_metadata",
279286
"repo_config",
280287
"compression_type",
288+
"layout",
281289
)
282290
model = RpmRepository
283291

@@ -395,6 +403,12 @@ class RpmPublicationSerializer(PublicationSerializer):
395403
choices=COMPRESSION_CHOICES,
396404
required=False,
397405
)
406+
layout = serializers.ChoiceField(
407+
help_text=_("How to layout the packages within the published repository."),
408+
choices=LAYOUT_CHOICES,
409+
required=False,
410+
allow_null=True,
411+
)
398412
gpgcheck = serializers.IntegerField(
399413
max_value=1,
400414
min_value=0,
@@ -476,6 +490,7 @@ class Meta:
476490
"sqlite_metadata",
477491
"repo_config",
478492
"compression_type",
493+
"layout",
479494
)
480495
model = RpmPublication
481496

pulp_rpm/app/tasks/publishing.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
CHECKSUM_TYPES,
2727
COMPRESSION_TYPES,
2828
PACKAGES_DIRECTORY,
29+
LAYOUT_TYPES,
2930
)
3031
from pulp_rpm.app.kickstart.treeinfo import PulpTreeInfo, TreeinfoData
3132
from pulp_rpm.app.models import (
@@ -328,6 +329,7 @@ def publish(
328329
checksum_type=None,
329330
repo_config=None,
330331
compression_type=COMPRESSION_TYPES.GZ,
332+
layout=LAYOUT_TYPES.NESTED_ALPHABETICALLY,
331333
*args,
332334
**kwargs,
333335
):
@@ -342,6 +344,8 @@ def publish(
342344
repo_config (JSON): repo config that will be served by distribution
343345
compression_type(pulp_rpm.app.constants.COMPRESSION_TYPES):
344346
Compression type to use for metadata files.
347+
layout(pulp_rpm.app.constants.LAYOUT_TYPES):
348+
How to layout the package files within the publication (flat, nested, etc.)
345349
346350
"""
347351
repository_version = RepositoryVersion.objects.get(pk=repository_version_pk)
@@ -369,6 +373,7 @@ def publish(
369373
publication.metadata_checksum_type = checksum_type
370374
publication.package_checksum_type = checksum_types.get("package") or checksum_type
371375
publication.compression_type = compression_type
376+
publication.layout = layout
372377
publication.repo_config = repo_config
373378

374379
publication_data = PublicationData(publication, checksum_types)
@@ -422,6 +427,7 @@ def generate_repo_metadata(
422427
sub_folder=None,
423428
metadata_signing_service=None,
424429
compression_type=COMPRESSION_TYPES.GZ,
430+
layout=LAYOUT_TYPES.NESTED_ALPHABETICALLY,
425431
):
426432
"""
427433
Creates a repomd.xml file.
@@ -435,6 +441,8 @@ def generate_repo_metadata(
435441
A reference to an associated signing service.
436442
compression_type(pulp_rpm.app.constants.COMPRESSION_TYPES):
437443
Compression type to use for metadata files.
444+
layout(pulp_rpm.app.constants.LAYOUT_TYPES):
445+
How to layout the package files within the publication (flat, nested, etc.)
438446
439447
"""
440448
cwd = os.getcwd()
@@ -565,11 +573,16 @@ def generate_repo_metadata(
565573
pkg.pkgId = pkgId
566574

567575
pkg_filename = os.path.basename(package.location_href)
568-
# this can cause an issue when two same RPM package names appears
569-
# a/name1.rpm b/name1.rpm
570-
pkg.location_href = os.path.join(
571-
PACKAGES_DIRECTORY, pkg_filename[0].lower(), pkg_filename
572-
)
576+
if layout == LAYOUT_TYPES.NESTED_ALPHABETICALLY:
577+
# this can cause an issue when two same RPM package names appears
578+
# a/name1.rpm b/name1.rpm
579+
pkg_path = os.path.join(PACKAGES_DIRECTORY, pkg_filename[0].lower(), pkg_filename)
580+
elif layout == LAYOUT_TYPES.FLAT:
581+
pkg_path = os.path.join(PACKAGES_DIRECTORY, pkg_filename)
582+
else:
583+
raise ValueError("Layout value is unsupported by this version")
584+
585+
pkg.location_href = pkg_path
573586

574587
if settings.RPM_METADATA_USE_REPO_PACKAGE_TIME:
575588
pkg.time_file = repo_pkg_times[package.pk]

pulp_rpm/tests/functional/api/test_publish.py

Lines changed: 81 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,81 @@ def test_publish_references_update(assert_created_publication):
224224
assert_created_publication(RPM_REFERENCES_UPDATEINFO_URL)
225225

226226

227+
def get_metadata_content_helper(base_url, repomd_elem, meta_type):
228+
"""Return the text contents of metadata file.
229+
230+
Provided a url, a repomd root element, and a metadata type, locate the metadata
231+
file's location href, download it from the provided url, un-gzip it, parse it, and
232+
return the root element node.
233+
234+
Don't use this with large repos because it will blow up.
235+
"""
236+
# <ns0:repomd xmlns:ns0="http://linux.duke.edu/metadata/repo">
237+
# <ns0:data type="primary">
238+
# <ns0:checksum type="sha256">[…]</ns0:checksum>
239+
# <ns0:location href="repodata/[…]-primary.xml.gz" />
240+
# …
241+
# </ns0:data>
242+
# …
243+
xpath = "{{{}}}data".format(RPM_NAMESPACES["metadata/repo"])
244+
data_elems = [elem for elem in repomd_elem.findall(xpath) if elem.get("type") == meta_type]
245+
if not data_elems:
246+
return None
247+
248+
xpath = "{{{}}}location".format(RPM_NAMESPACES["metadata/repo"])
249+
location_href = data_elems[0].find(xpath).get("href")
250+
251+
return download_and_decompress_file(os.path.join(base_url, location_href))
252+
253+
254+
@pytest.mark.parametrize("layout", ["flat", "nested_alphabetically"])
255+
def test_repo_layout(
256+
layout,
257+
init_and_sync,
258+
rpm_publication_api,
259+
gen_object_with_cleanup,
260+
rpm_distribution_api,
261+
rpm_distribution_factory,
262+
monitor_task,
263+
delete_orphans_pre,
264+
tmpdir,
265+
wget_recursive_download_on_host,
266+
):
267+
"""Test that using the "layout" option for publication produces the correct package layouts"""
268+
269+
# create repo and remote
270+
repo, _ = init_and_sync(url=RPM_UNSIGNED_FIXTURE_URL, policy="on_demand")
271+
272+
# publish
273+
publish_data = RpmRpmPublication(repository=repo.pulp_href, layout=layout)
274+
publish_response = rpm_publication_api.create(publish_data)
275+
created_resources = monitor_task(publish_response.task).created_resources
276+
publication_href = created_resources[0]
277+
278+
# distribute
279+
distribution = rpm_distribution_factory(publication=publication_href)
280+
281+
# Download and parse the metadata.
282+
repomd = ElementTree.fromstring(
283+
requests.get(os.path.join(distribution.base_url, "repodata/repomd.xml")).text
284+
)
285+
286+
# Convert the metadata into a more workable form and then compare.
287+
primary = get_metadata_content_helper(distribution.base_url, repomd, "primary")
288+
289+
packages = xmltodict.parse(primary, dict_constructor=collections.OrderedDict)["metadata"][
290+
"package"
291+
]
292+
293+
for package in packages:
294+
if layout == "flat":
295+
assert package["location"]["@href"].startswith("Packages/{}".format(package["name"][0]))
296+
elif layout == "nested_alphabetically":
297+
assert package["location"]["@href"].startswith(
298+
"Packages/{}/".format(package["name"][0])
299+
)
300+
301+
227302
@pytest.mark.parametrize("repo_url", [RPM_COMPLEX_FIXTURE_URL, RPM_MODULAR_FIXTURE_URL])
228303
def test_complex_repo_core_metadata(
229304
distribution_base_url,
@@ -265,45 +340,19 @@ def test_complex_repo_core_metadata(
265340
).text
266341
)
267342

268-
def get_metadata_content(base_url, repomd_elem, meta_type):
269-
"""Return the text contents of metadata file.
270-
271-
Provided a url, a repomd root element, and a metadata type, locate the metadata
272-
file's location href, download it from the provided url, un-gzip it, parse it, and
273-
return the root element node.
274-
275-
Don't use this with large repos because it will blow up.
276-
"""
277-
# <ns0:repomd xmlns:ns0="http://linux.duke.edu/metadata/repo">
278-
# <ns0:data type="primary">
279-
# <ns0:checksum type="sha256">[…]</ns0:checksum>
280-
# <ns0:location href="repodata/[…]-primary.xml.gz" />
281-
# …
282-
# </ns0:data>
283-
# …
284-
xpath = "{{{}}}data".format(RPM_NAMESPACES["metadata/repo"])
285-
data_elems = [elem for elem in repomd_elem.findall(xpath) if elem.get("type") == meta_type]
286-
if not data_elems:
287-
return None
288-
289-
xpath = "{{{}}}location".format(RPM_NAMESPACES["metadata/repo"])
290-
location_href = data_elems[0].find(xpath).get("href")
291-
292-
return download_and_decompress_file(os.path.join(base_url, location_href))
293-
294343
# Convert the metadata into a more workable form and then compare.
295344
for metadata_file in ["primary", "filelists", "other"]:
296-
original_metadata = get_metadata_content(repo_url, original_repomd, metadata_file)
297-
generated_metadata = get_metadata_content(
345+
original_metadata = get_metadata_content_helper(repo_url, original_repomd, metadata_file)
346+
generated_metadata = get_metadata_content_helper(
298347
distribution_base_url(distribution.base_url), reproduced_repomd, metadata_file
299348
)
300349

301350
_compare_xml_metadata_file(original_metadata, generated_metadata, metadata_file)
302351

303352
# =================
304353

305-
original_modulemds = get_metadata_content(repo_url, original_repomd, "modules")
306-
generated_modulemds = get_metadata_content(
354+
original_modulemds = get_metadata_content_helper(repo_url, original_repomd, "modules")
355+
generated_modulemds = get_metadata_content_helper(
307356
distribution_base_url(distribution.base_url), reproduced_repomd, "modules"
308357
)
309358

@@ -319,8 +368,8 @@ def get_metadata_content(base_url, repomd_elem, meta_type):
319368
# ===================
320369

321370
# TODO: make this deeper
322-
original_updateinfo = get_metadata_content(repo_url, original_repomd, "updateinfo")
323-
generated_updateinfo = get_metadata_content(
371+
original_updateinfo = get_metadata_content_helper(repo_url, original_repomd, "updateinfo")
372+
generated_updateinfo = get_metadata_content_helper(
324373
distribution_base_url(distribution.base_url), reproduced_repomd, "updateinfo"
325374
)
326375
assert bool(original_updateinfo) == bool(generated_updateinfo)

0 commit comments

Comments
 (0)