Skip to content

Commit bb93616

Browse files
committed
feat(downloader): resolve latest via latest pointer target
1 parent e29f0e3 commit bb93616

2 files changed

Lines changed: 29 additions & 104 deletions

File tree

datadog_checks_downloader/datadog_checks/downloader/download_v2.py

Lines changed: 14 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
The v2 format stores a JSON pointer file as a TUF target at:
88
99
targets/<project>/<version>.json (versioned pointer)
10+
targets/<project>/latest.json (copy of latest stable version pointer)
1011
1112
Target files are content-addressed on S3 (consistent-snapshot format):
12-
the actual file at ``targets/<project>/<version>.json`` is stored as
13-
``targets/<project>/{sha256}.{version}.json``. The TUF Updater resolves
14-
the hash-prefixed path automatically via ``N.targets.json`` metadata.
13+
the actual file at ``targets/<project>/<name>.json`` is stored as
14+
``targets/<project>/{sha256}.{name}.json``. The TUF Updater resolves
15+
the hash-prefixed path automatically via TUF metadata.
1516
1617
Each pointer file contains:
1718
@@ -38,7 +39,6 @@
3839
import urllib.request
3940
from pathlib import Path
4041

41-
from packaging.version import InvalidVersion, Version
4242
from tuf.api.metadata import Metadata, Snapshot, Targets, Timestamp
4343
from tuf.ngclient import Updater
4444
from tuf.ngclient.config import UpdaterConfig
@@ -99,32 +99,9 @@ def _make_updater(self, metadata_dir: Path, target_dir: Path) -> Updater:
9999
)
100100

101101
@staticmethod
102-
def _resolve_version(project: str, version: str | None, targets: Targets) -> str:
103-
"""Return the concrete version to fetch.
104-
105-
If *version* is given, return it as-is (TUF will raise if absent).
106-
If None, scan *targets* for all ``<project>/<ver>.json`` entries and
107-
return the highest stable PEP 440 version.
108-
"""
109-
if version is not None:
110-
return version
111-
112-
prefix = f"{project}/"
113-
stable: list[Version] = []
114-
for target_path in targets.targets:
115-
if not (target_path.startswith(prefix) and target_path.endswith(".json")):
116-
continue
117-
stem = target_path[len(prefix): -len(".json")]
118-
try:
119-
v = Version(stem)
120-
except InvalidVersion:
121-
continue
122-
if not v.is_prerelease:
123-
stable.append(v)
124-
125-
if not stable:
126-
raise TargetNotFoundError(f"No stable releases found for {project!r} in targets metadata")
127-
return str(max(stable))
102+
def _target_path(project: str, version: str | None) -> str:
103+
name = version if version is not None else 'latest'
104+
return f'{project}/{name}.json'
128105

129106
def _fetch_targets_unverified(self) -> Targets:
130107
"""Fetch targets metadata without verifying TUF signatures.
@@ -152,9 +129,7 @@ def _fetch_targets_unverified(self) -> Targets:
152129
def get_pointer(self, project: str, version: str | None = None) -> dict:
153130
"""Return the pointer JSON for *project* at *version*.
154131
155-
Resolves *version* = None to the latest stable release by scanning
156-
``N.targets.json``. The pointer file is fetched via its hash-prefixed
157-
consistent-snapshot path.
132+
Resolves *version* = None by fetching ``<project>/latest.json``.
158133
Raises ``TargetNotFoundError`` if the target is absent from the TUF repo.
159134
"""
160135
with tempfile.TemporaryDirectory() as tmp:
@@ -163,17 +138,17 @@ def get_pointer(self, project: str, version: str | None = None) -> dict:
163138
metadata_dir.mkdir()
164139
target_dir.mkdir()
165140

141+
target_path = self._target_path(project, version)
142+
target_name = Path(target_path).stem
143+
166144
if self._disable_verification:
167145
targets = self._fetch_targets_unverified()
168-
resolved = self._resolve_version(project, version, targets)
169-
target_path = f'{project}/{resolved}.json'
170-
171146
entry = targets.targets.get(target_path)
172147
if entry is None:
173148
raise TargetNotFoundError(f'Target not found: {target_path}')
174149

175150
sha256 = entry.hashes.get('sha256', '')
176-
url = f'{self._repository_url}/targets/{project}/{sha256}.{resolved}.json'
151+
url = f'{self._repository_url}/targets/{project}/{sha256}.{target_name}.json'
177152
try:
178153
with urllib.request.urlopen(url) as resp:
179154
return json.loads(resp.read())
@@ -186,19 +161,10 @@ def get_pointer(self, project: str, version: str | None = None) -> dict:
186161
updater = self._make_updater(metadata_dir, target_dir)
187162
updater.refresh()
188163

189-
# Parse the downloaded N.targets.json to resolve version and
190-
# enumerate the hash-prefixed path the Updater will fetch.
191-
candidates = sorted(metadata_dir.glob('*.targets.json'))
192-
if not candidates:
193-
raise TargetNotFoundError(f'No targets metadata after refresh for {project!r}')
194-
targets = Metadata[Targets].from_file(str(candidates[-1])).signed
195-
196-
resolved = self._resolve_version(project, version, targets)
197-
target_path = f'{project}/{resolved}.json'
198-
199164
target_info = updater.get_targetinfo(target_path)
200165
if target_info is None:
201-
raise TargetNotFoundError(f'No TUF target for {project!r} version {resolved!r}')
166+
label = version if version is not None else 'latest stable'
167+
raise TargetNotFoundError(f'No TUF target for {project!r} version {label!r}')
202168

203169
pointer_path = target_dir / target_path
204170
pointer_path.parent.mkdir(parents=True, exist_ok=True)

datadog_checks_downloader/tests/test_v2_downloader.py

Lines changed: 15 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,9 @@
1515
from unittest.mock import MagicMock, patch
1616

1717
import pytest
18-
from packaging.version import Version
19-
from tuf.api.metadata import MetaFile, Snapshot, Targets, Timestamp
20-
2118
from datadog_checks.downloader.download_v2 import TUFPointerDownloader
2219
from datadog_checks.downloader.exceptions import DigestMismatch, TargetNotFoundError
20+
from tuf.api.metadata import Targets
2321

2422
# ---------------------------------------------------------------------------
2523
# Shared fixtures
@@ -90,47 +88,16 @@ def fake_from_file(path):
9088

9189

9290
# ---------------------------------------------------------------------------
93-
# _resolve_version unit tests
91+
# target-path unit tests
9492
# ---------------------------------------------------------------------------
9593

9694

97-
class TestResolveVersion:
98-
def test_explicit_version_returned_unchanged(self):
99-
targets = _make_targets('1.0.0', '2.0.0')
100-
assert TUFPointerDownloader._resolve_version(_PROJECT, '1.0.0', targets) == '1.0.0'
101-
102-
def test_latest_picks_highest_stable(self):
103-
targets = _make_targets('1.0.0', '2.0.0', '1.5.0')
104-
result = TUFPointerDownloader._resolve_version(_PROJECT, None, targets)
105-
assert result == '2.0.0'
106-
107-
def test_latest_excludes_prereleases(self):
108-
targets = _make_targets('1.0.0', '2.0.0rc1', '1.9.0b2', '1.8.0a1', '1.7.0.dev1')
109-
result = TUFPointerDownloader._resolve_version(_PROJECT, None, targets)
110-
assert result == '1.0.0'
111-
112-
def test_latest_includes_post_releases(self):
113-
targets = _make_targets('1.0.0', '1.0.0.post1')
114-
result = TUFPointerDownloader._resolve_version(_PROJECT, None, targets)
115-
assert result == '1.0.0.post1'
95+
class TestTargetPath:
96+
def test_explicit_version_uses_versioned_pointer(self):
97+
assert TUFPointerDownloader._target_path(_PROJECT, '1.0.0') == f'{_PROJECT}/1.0.0.json'
11698

117-
def test_no_stable_versions_raises(self):
118-
targets = _make_targets('1.0.0rc1', '2.0.0b1')
119-
with pytest.raises(TargetNotFoundError, match=_PROJECT):
120-
TUFPointerDownloader._resolve_version(_PROJECT, None, targets)
121-
122-
def test_ignores_other_projects(self):
123-
from tuf.api.metadata import TargetFile
124-
125-
targets = Targets()
126-
for path in ['datadog-mysql/1.0.0.json', f'{_PROJECT}/2.0.0.json']:
127-
tf = MagicMock(spec=TargetFile)
128-
tf.hashes = {'sha256': 'a' * 64}
129-
tf.length = 100
130-
targets.targets[path] = tf
131-
132-
result = TUFPointerDownloader._resolve_version(_PROJECT, None, targets)
133-
assert result == '2.0.0'
99+
def test_missing_version_uses_latest_pointer(self):
100+
assert TUFPointerDownloader._target_path(_PROJECT, None) == f'{_PROJECT}/latest.json'
134101

135102

136103
# ---------------------------------------------------------------------------
@@ -163,7 +130,9 @@ def test_download_returns_wheel_path(self, mock_urlopen, mock_metadata_cls, mock
163130
@patch('datadog_checks.downloader.download_v2.Updater')
164131
@patch('datadog_checks.downloader.download_v2.Metadata')
165132
@patch('datadog_checks.downloader.download_v2.urllib.request.urlopen')
166-
def test_repository_flag_overrides_pointer_repository(self, mock_urlopen, mock_metadata_cls, mock_updater_cls, tmp_path):
133+
def test_repository_flag_overrides_pointer_repository(
134+
self, mock_urlopen, mock_metadata_cls, mock_updater_cls, tmp_path
135+
):
167136
"""--repository supersedes the repository field baked into the pointer."""
168137
staging_url = 'https://agent-integration-wheels-staging.s3.amazonaws.com'
169138
prod_pointer = {**_POINTER, 'repository': 'https://agent-integration-wheels-prod.s3.amazonaws.com'}
@@ -195,24 +164,20 @@ def fake_urlopen(url):
195164
@patch('datadog_checks.downloader.download_v2.Updater')
196165
@patch('datadog_checks.downloader.download_v2.Metadata')
197166
@patch('datadog_checks.downloader.download_v2.urllib.request.urlopen')
198-
def test_version_none_resolves_to_latest_stable(self, mock_urlopen, mock_metadata_cls, mock_updater_cls, tmp_path):
199-
"""version=None picks the highest stable version from N.targets.json."""
200-
targets = _make_targets('1.0.0', '2.0.0', '3.0.0rc1')
167+
def test_version_none_fetches_latest_pointer(self, mock_urlopen, mock_metadata_cls, mock_updater_cls, tmp_path):
168+
"""version=None fetches <project>/latest.json."""
201169
mock_updater = _mock_tuf_updater(_POINTER)
202170
mock_updater_cls.return_value = mock_updater
203-
mock_metadata_cls.__getitem__.return_value.from_file.return_value.signed = targets
204171

205172
mock_urlopen.return_value.__enter__ = lambda s: s
206173
mock_urlopen.return_value.__exit__ = MagicMock(return_value=False)
207174
mock_urlopen.return_value.read.return_value = _WHEEL_CONTENT
208175

209-
with patch('datadog_checks.downloader.download_v2.Path.glob', return_value=iter(['1.targets.json'])):
210-
downloader = TUFPointerDownloader(repository_url=_REPO_URL)
211-
downloader.download(_PROJECT, dest_dir=tmp_path)
176+
downloader = TUFPointerDownloader(repository_url=_REPO_URL)
177+
downloader.download(_PROJECT, dest_dir=tmp_path)
212178

213-
# Updater should have been asked for the 2.0.0 target, not 3.0.0rc1
214179
call_args = mock_updater.get_targetinfo.call_args[0][0]
215-
assert call_args == f'{_PROJECT}/2.0.0.json'
180+
assert call_args == f'{_PROJECT}/latest.json'
216181

217182

218183
# ---------------------------------------------------------------------------
@@ -287,8 +252,6 @@ class TestDisableVerification:
287252
def test_skips_tuf_and_digest(self, mock_urlopen, tmp_path):
288253
"""disable_verification fetches metadata chain to find hash-prefixed path,
289254
then downloads without digest checks."""
290-
from tuf.api.metadata import MetaFile, TargetFile
291-
292255
# Build fake metadata responses
293256
ts = MagicMock()
294257
ts.snapshot_meta.version = 1
@@ -300,7 +263,6 @@ def test_skips_tuf_and_digest(self, mock_urlopen, tmp_path):
300263
snap_md = MagicMock()
301264
snap_md.signed = snap
302265

303-
from tuf.api.metadata import TargetFile as TF
304266
tf = MagicMock()
305267
tf.hashes = {'sha256': 'a' * 64}
306268
targets = Targets()
@@ -316,7 +278,6 @@ def fake_from_bytes(data):
316278
call_idx[0] += 1
317279
return r
318280

319-
pointer_bytes = json.dumps(_POINTER).encode()
320281
bad_pointer_bytes = json.dumps({**_POINTER, 'digest': 'deadbeef' * 8}).encode()
321282

322283
url_call = [0]
@@ -340,8 +301,6 @@ def fake_urlopen(url):
340301

341302
@patch('datadog_checks.downloader.download_v2.urllib.request.urlopen')
342303
def test_404_raises_target_not_found(self, mock_urlopen):
343-
from tuf.api.metadata import TargetFile
344-
345304
ts = MagicMock()
346305
ts.snapshot_meta.version = 1
347306
ts_md = MagicMock()

0 commit comments

Comments
 (0)