Skip to content

Commit e27bba1

Browse files
authored
Merge pull request #86 from jajreidy/fix-url-bug
**feat(upload): per-arch RPM repos, signed RPM result URLs, clearer repo/distro logs**
2 parents d18a77f + 7bfec75 commit e27bba1

16 files changed

+336
-158
lines changed

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Added
11-
- `upload --target-arch-repo`: RPM-only; per-architecture RPM repos/distributions (e.g. `{namespace}/{arch}/`); logs/SBOM/artifacts remain build-scoped; lazy repo creation at upload; supported with `--results-json`, `--signed-by`, and `--overwrite`
11+
- `upload --target-arch-repo`: per-architecture RPM repos/distributions (``{namespace}/{arch}/Packages/...``); logs/SBOM/artifacts stay build-scoped; lazy repo creation at upload; works with `--results-json`, `--signed-by`, and `--overwrite`; with `--signed-by`, same arch repo and `signed_by` is label-only
1212
- `upload --overwrite`: RPM-only; remove existing RPM package units in the target repo that match local file SHA256 (and `signed_by` when set) via `remove_content_units` before upload
1313
- `upload --results-json`: Upload artifacts from pulp_results.json; files resolved from JSON directory or --files-base-path; --build-id and --namespace optional (extracted from artifact labels)
1414
- DistributionClient username/password (Basic Auth) support; use `username` and `password` in config as alternative to cert/key for pull downloads
@@ -21,10 +21,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2121
- Username/password (Basic Auth) support for packages.redhat.com
2222

2323
### Changed
24-
- `upload --target-arch-repo` with `--signed-by`: RPM repo/distribution paths remain `{arch}/` only (no `{arch}/rpms-signed`); signing is via `signed_by` label on content
24+
- Repository setup logs use the concrete repo slug (e.g. ``rpms-signed``) instead of a generic ``Rpms`` label; distribution creation logs state that ``name`` and ``base_path`` match the repository name on one line
25+
- `upload --target-arch-repo` with `--signed-by`: RPM paths remain `{arch}/` only (no `{arch}/rpms-signed`); signing is via `signed_by` label on content
2526
- `pull`: use each artifact's ``url`` from pulp_results.json when present instead of synthesizing download URLs from distribution entries
2627

2728
### Fixed
29+
- Results JSON RPM URLs with `--signed-by`: use the `rpms-signed` distribution base (`distributions.rpms_signed` / correct artifact `url`) instead of the unsigned `rpms` path
2830
- RPM distribution URLs: ``Packages/<letter>/`` uses the lowercase first character of the RPM **basename** only (correct for paths like ``Packages/W/foo.rpm``, ``arch/pkg.rpm``, or plain ``foo.rpm``)
2931
- Clear error when no auth credentials provided (client_id/client_secret or username/password)
3032

pulp_tool/api/pulp_client.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,9 +1078,19 @@ def _rpm_distribution_base_url_from_labels(self, labels: Dict[str, str]) -> str:
10781078
arch_seg = sanitize_build_id_for_repository(arch)
10791079
if not validate_build_id(arch_seg):
10801080
arch_seg = "noarch"
1081-
# Per-arch repos do not use a separate *-signed distribution; signed_by is label-only.
10821081
return f"{pulp_content}{ns}/{arch_seg}/"
10831082

1083+
def _build_rpm_packages_url_for_arch(self, arch: str, relative_path: str) -> str:
1084+
"""Build RPM content URL under per-arch distribution (namespace/arch/Packages/...)."""
1085+
base_url_str = str(self.config["base_url"]).rstrip("/")
1086+
pulp_content_base_url = f"{base_url_str}/api/pulp-content"
1087+
ns = self.namespace if isinstance(self.namespace, str) else ""
1088+
base = f"{pulp_content_base_url}/{ns}/{arch}/"
1089+
filename, first_letter = rpm_packages_letter_and_basename(relative_path)
1090+
if filename:
1091+
return f"{base}Packages/{first_letter}/{filename}"
1092+
return relative_path
1093+
10841094
def _build_rpm_distribution_url(
10851095
self,
10861096
relative_path: str,
@@ -1091,11 +1101,22 @@ def _build_rpm_distribution_url(
10911101
) -> str:
10921102
"""Build distribution URL for RPM artifacts (Packages/<lowercase-first-of-basename>/<basename>)."""
10931103
labels = labels or {}
1094-
rpms_url = ""
10951104
if target_arch_repo:
1105+
arch = (labels.get("arch") or "").strip()
1106+
if arch and arch in SUPPORTED_ARCHITECTURES:
1107+
return self._build_rpm_packages_url_for_arch(arch, relative_path)
10961108
rpms_url = self._rpm_distribution_base_url_from_labels(labels)
1097-
if not rpms_url:
1098-
rpms_url = distribution_urls.get("rpms", "")
1109+
filename, first_letter = rpm_packages_letter_and_basename(relative_path)
1110+
if filename:
1111+
return f"{rpms_url}Packages/{first_letter}/{filename}"
1112+
return relative_path
1113+
if labels.get("signed_by") and distribution_urls.get("rpms_signed"):
1114+
rpms_url = distribution_urls["rpms_signed"]
1115+
if rpms_url:
1116+
filename, first_letter = rpm_packages_letter_and_basename(relative_path)
1117+
if filename:
1118+
return f"{rpms_url}Packages/{first_letter}/{filename}"
1119+
rpms_url = distribution_urls.get("rpms", "")
10991120
if rpms_url:
11001121
filename, first_letter = rpm_packages_letter_and_basename(relative_path)
11011122
if filename:
@@ -1150,14 +1171,17 @@ def _build_artifact_distribution_url(
11501171
is_rpm: Whether this is an RPM artifact
11511172
labels: Labels from content (may contain arch, etc.)
11521173
distribution_urls: Dictionary mapping repo_type to distribution base URL
1153-
target_arch_repo: When True, RPM base URLs are derived per architecture from labels
1174+
target_arch_repo: When True, RPM URLs use per-arch paths from labels (or sanitized fallback)
11541175
11551176
Returns:
11561177
Full distribution URL for the artifact
11571178
"""
11581179
if is_rpm:
11591180
return self._build_rpm_distribution_url(
1160-
relative_path, distribution_urls, labels, target_arch_repo=target_arch_repo
1181+
relative_path,
1182+
distribution_urls,
1183+
labels=labels,
1184+
target_arch_repo=target_arch_repo,
11611185
)
11621186
return PulpClient._build_file_distribution_url(relative_path, labels, distribution_urls)
11631187

@@ -1746,7 +1770,7 @@ def build_results_structure(
17461770
content_results: Content data from Pulp
17471771
file_info_map: Mapping of artifact hrefs to file info models
17481772
distribution_urls: Optional dictionary mapping repo_type to distribution base URL
1749-
target_arch_repo: When True, RPM artifact URLs use per-arch distribution paths from labels
1773+
target_arch_repo: When True, RPM URLs use per-arch distribution paths from labels
17501774
17511775
Returns:
17521776
Populated PulpResultsModel

pulp_tool/cli/upload.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ def _extract_build_id_namespace_from_results_json(results_json_path: Path) -> Tu
110110
default=False,
111111
help=(
112112
"RPM only: use each architecture as the RPM repository/distribution name "
113-
"(e.g. .../pulp-content/{namespace}/x86_64/) instead of {build}/rpms; logs/SBOM/artifacts stay build-scoped"
113+
"(e.g. .../pulp-content/{namespace}/x86_64/Packages/...) instead of {build}/rpms; "
114+
"logs/SBOM/artifacts stay build-scoped. With --signed-by, same arch repo; signed_by is label-only."
114115
),
115116
)
116117
@click.pass_context
@@ -192,7 +193,7 @@ def upload( # pylint: disable=too-many-arguments,too-many-positional-arguments
192193
build_id,
193194
signed_by=args.signed_by,
194195
skip_artifacts_repo=skip_artifacts,
195-
target_arch_repo=target_arch_repo,
196+
target_arch_repo=args.target_arch_repo,
196197
)
197198
logging.info("Repository setup completed")
198199

pulp_tool/services/upload_service.py

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,17 @@ def upload_sbom(
163163
return task_response.created_resources
164164

165165

166+
def _distribution_urls_for_context(helper: PulpHelper, build_id: str, context: UploadContext) -> Dict[str, str]:
167+
"""Resolve distribution URL map for results JSON (per-arch vs signed aggregate RPM base)."""
168+
target_arch = bool(getattr(context, "target_arch_repo", False))
169+
signed_by = getattr(context, "signed_by", None)
170+
if target_arch:
171+
return helper.get_distribution_urls(build_id, target_arch_repo=True)
172+
if signed_by and str(signed_by).strip():
173+
return helper.get_distribution_urls(build_id, include_signed_rpm_distro=True)
174+
return helper.get_distribution_urls(build_id)
175+
176+
166177
def _classify_artifact_from_key(key: str) -> str:
167178
"""
168179
Classify artifact type from key (path/filename).
@@ -200,7 +211,7 @@ def process_uploads_from_results_json(
200211
client: PulpClient instance
201212
context: UploadRpmContext with results_json, files_base_path, signed_by
202213
repositories: RepositoryRefs (with signed refs when signed_by set)
203-
pulp_helper: PulpHelper for per-arch RPM repos when ``target_arch_repo`` is set
214+
pulp_helper: Optional PulpHelper for per-arch RPM repos when ``target_arch_repo`` is set
204215
205216
Returns:
206217
URL of the uploaded results JSON, or None if upload failed
@@ -228,16 +239,15 @@ def process_uploads_from_results_json(
228239
base_path = Path(context.files_base_path or os.path.dirname(context.results_json)).resolve()
229240
use_signed = bool(context.signed_by and context.signed_by.strip())
230241

231-
# Only RPMs use signed repos; logs and SBOMs are never signed
232-
if context.target_arch_repo:
233-
rpm_href = ""
234-
else:
235-
rpm_href = repositories.rpms_signed_href if use_signed else repositories.rpms_href
242+
# Only RPMs use signed aggregate repo; with target_arch_repo, signed_by is label-only on arch repos
243+
rpm_href = (
244+
"" if context.target_arch_repo else (repositories.rpms_signed_href if use_signed else repositories.rpms_href)
245+
)
236246
logs_prn = repositories.logs_prn
237247
sbom_prn = repositories.sbom_prn
238248
artifacts_prn = repositories.artifacts_prn
239249

240-
if not context.target_arch_repo and use_signed and not rpm_href and not repositories.rpms_signed_prn:
250+
if use_signed and not context.target_arch_repo and not rpm_href and not repositories.rpms_signed_prn:
241251
logging.error("signed_by set but signed repositories not available")
242252
raise ValueError("signed_by requires signed repositories")
243253

@@ -302,7 +312,9 @@ def process_uploads_from_results_json(
302312

303313
# Upload RPMs
304314
for arch, rpm_list in rpms_by_arch.items():
305-
arch_href = helper.ensure_rpm_repository_for_arch(arch) if context.target_arch_repo else rpm_href
315+
arch_href = (
316+
helper.ensure_rpm_repository_for_arch(context.build_id, arch) if context.target_arch_repo else rpm_href
317+
)
306318
created_resources.extend(
307319
upload_rpms(
308320
rpm_list,
@@ -583,11 +595,8 @@ def _populate_results_model(
583595
"""
584596
# Get distribution URLs to construct proper artifact URLs
585597
repository_helper = PulpHelper(client, parent_package=context.parent_package)
586-
target_arch_repo = getattr(context, "target_arch_repo", False)
587-
if target_arch_repo:
588-
distribution_urls = repository_helper.get_distribution_urls(context.build_id, target_arch_repo=True)
589-
else:
590-
distribution_urls = repository_helper.get_distribution_urls(context.build_id)
598+
distribution_urls = _distribution_urls_for_context(repository_helper, context.build_id, context)
599+
target_arch_repo = bool(getattr(context, "target_arch_repo", False))
591600

592601
client.build_results_structure(
593602
results_model,
@@ -610,11 +619,7 @@ def _add_distributions_to_results(
610619
results_model: Model to add distributions to
611620
"""
612621
repository_helper = PulpHelper(client, parent_package=context.parent_package)
613-
target_arch_repo = getattr(context, "target_arch_repo", False)
614-
if target_arch_repo:
615-
distribution_urls = repository_helper.get_distribution_urls(context.build_id, target_arch_repo=True)
616-
else:
617-
distribution_urls = repository_helper.get_distribution_urls(context.build_id)
622+
distribution_urls = _distribution_urls_for_context(repository_helper, context.build_id, context)
618623

619624
if distribution_urls:
620625
for repo_type, url in distribution_urls.items():

pulp_tool/utils/distribution_manager.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,13 @@ def __init__(
3939
distribution_cache if distribution_cache is not None else {}
4040
)
4141

42-
def get_distribution_urls(self, build_id: str, target_arch_repo: bool = False) -> Dict[str, str]:
42+
def get_distribution_urls(
43+
self,
44+
build_id: str,
45+
*,
46+
target_arch_repo: bool = False,
47+
include_signed_rpm_distro: bool = False,
48+
) -> Dict[str, str]:
4349
"""
4450
Get distribution URLs for all repository types.
4551
@@ -48,7 +54,8 @@ def get_distribution_urls(self, build_id: str, target_arch_repo: bool = False) -
4854
4955
Args:
5056
build_id: Build ID for naming repositories and distributions
51-
target_arch_repo: If True, omit the aggregate ``rpms`` URL (per-arch RPM URLs are built elsewhere)
57+
target_arch_repo: If True, omit aggregate ``rpms`` URL (per-arch RPM URLs elsewhere)
58+
include_signed_rpm_distro: If True, add ``rpms_signed`` URL for ``{build}/rpms-signed/``
5259
5360
Returns:
5461
Dictionary mapping repo_type to distribution URL
@@ -69,7 +76,11 @@ def get_distribution_urls(self, build_id: str, target_arch_repo: bool = False) -
6976
logging.debug("Getting distribution URLs for build: %s", sanitized_build_id)
7077

7178
# Get distribution URLs directly using the helper's own methods
72-
distribution_urls = self._get_distribution_urls_impl(sanitized_build_id, target_arch_repo=target_arch_repo)
79+
distribution_urls = self._get_distribution_urls_impl(
80+
sanitized_build_id,
81+
target_arch_repo=target_arch_repo,
82+
include_signed_rpm_distro=include_signed_rpm_distro,
83+
)
7384

7485
logging.debug("Retrieved %d distribution URLs", len(distribution_urls))
7586
return distribution_urls
@@ -108,12 +119,20 @@ def _get_single_distribution_url(self, build_id: str, repo_type: str, base_url:
108119
)
109120
return distribution_url
110121

111-
def _get_distribution_urls_impl(self, build_id: str, target_arch_repo: bool = False) -> Dict[str, str]:
122+
def _get_distribution_urls_impl(
123+
self,
124+
build_id: str,
125+
*,
126+
target_arch_repo: bool = False,
127+
include_signed_rpm_distro: bool = False,
128+
) -> Dict[str, str]:
112129
"""
113130
Get distribution URLs for all repository types.
114131
115132
Args:
116133
build_id: Base name for the repositories (may include namespace prefix)
134+
target_arch_repo: If True, omit aggregate ``rpms`` URL
135+
include_signed_rpm_distro: If True, add ``rpms_signed`` for signed RPM distribution
117136
118137
Returns:
119138
Dictionary mapping repo_type to distribution URL
@@ -132,6 +151,11 @@ def _get_distribution_urls_impl(self, build_id: str, target_arch_repo: bool = Fa
132151
if url:
133152
distribution_urls[repo_type] = url
134153

154+
if include_signed_rpm_distro and not target_arch_repo:
155+
signed_url = self._get_single_distribution_url(build_id, "rpms-signed", base_url)
156+
if signed_url:
157+
distribution_urls["rpms_signed"] = signed_url
158+
135159
return distribution_urls
136160

137161

pulp_tool/utils/pulp_helper.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,9 @@ def setup_repositories(
6666
6767
Args:
6868
build_id: Build ID for naming repositories and distributions
69-
signed_by: If set, also create signed repos (rpms-signed, etc.)
69+
signed_by: If set, also create signed repos (rpms-signed, etc.) unless target_arch_repo
7070
skip_artifacts_repo: If True, do not create artifacts repo (e.g. when saving locally)
71-
target_arch_repo: If True, skip bulk rpms/rpms-signed; use ensure_rpm_repository_for_arch per arch
71+
target_arch_repo: If True, skip aggregate rpms/rpms-signed; per-arch RPM repos at upload time
7272
7373
Returns:
7474
RepositoryRefs NamedTuple containing all repository PRNs and hrefs
@@ -80,16 +80,13 @@ def setup_repositories(
8080
target_arch_repo=target_arch_repo,
8181
)
8282

83-
def ensure_rpm_repository_for_arch(self, arch: str) -> str:
84-
"""
85-
Create or get the RPM repository for an architecture (target-arch-repo mode).
86-
87-
Returns:
88-
Repository pulp_href for modify/add_content
89-
"""
90-
return self._repository_manager.ensure_rpm_repository_for_arch(arch)
91-
92-
def get_distribution_urls(self, build_id: str, target_arch_repo: bool = False) -> dict[str, str]:
83+
def get_distribution_urls(
84+
self,
85+
build_id: str,
86+
*,
87+
target_arch_repo: bool = False,
88+
include_signed_rpm_distro: bool = False,
89+
) -> dict[str, str]:
9390
"""
9491
Get distribution URLs for all repository types.
9592
@@ -98,11 +95,21 @@ def get_distribution_urls(self, build_id: str, target_arch_repo: bool = False) -
9895
9996
Args:
10097
build_id: Build ID for naming repositories and distributions
98+
target_arch_repo: If True, omit aggregate ``rpms`` distribution URL
99+
include_signed_rpm_distro: If True, include ``rpms_signed`` URL
101100
102101
Returns:
103102
Dictionary mapping repo_type to distribution URL
104103
"""
105-
return self._distribution_manager.get_distribution_urls(build_id)
104+
return self._distribution_manager.get_distribution_urls(
105+
build_id,
106+
target_arch_repo=target_arch_repo,
107+
include_signed_rpm_distro=include_signed_rpm_distro,
108+
)
109+
110+
def ensure_rpm_repository_for_arch(self, build_id: str, arch: str) -> str:
111+
"""Create or get RPM repository for ``target_arch_repo`` mode (base_path = arch)."""
112+
return self._repository_manager.ensure_rpm_repository_for_arch(build_id, arch)
106113

107114
def create_or_get_repository(
108115
self,

0 commit comments

Comments
 (0)