Skip to content

Commit e29f0e3

Browse files
dkirov-ddclaude
andcommitted
feat(downloader): resolve hash-prefixed targets via N.targets.json
The v2 TUF repository uses consistent-snapshot format: pointer files are stored as {sha256}.{version}.json on S3. Two changes to support this: 1. _make_updater now sets UpdaterConfig(prefix_targets_with_hash=True) so the TUF Updater resolves hash-prefixed paths automatically when calling download_target(). 2. get_pointer() now parses N.targets.json (after Updater.refresh()) to enumerate available versions for the project. This replaces the removed latest.json: when version=None, _resolve_version() scans all <project>/<ver>.json entries in targets metadata and returns the highest stable PEP 440 version. The disable_verification path fetches the metadata chain (timestamp → snapshot → targets) without verifying signatures to find the hash-prefixed URL, then fetches the pointer directly. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 2df1e0e commit e29f0e3

2 files changed

Lines changed: 310 additions & 115 deletions

File tree

datadog_checks_downloader/datadog_checks/downloader/download_v2.py

Lines changed: 84 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
99
targets/<project>/<version>.json (versioned pointer)
1010
11+
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.
15+
1116
Each pointer file contains:
1217
1318
{
@@ -33,14 +38,17 @@
3338
import urllib.request
3439
from pathlib import Path
3540

41+
from packaging.version import InvalidVersion, Version
42+
from tuf.api.metadata import Metadata, Snapshot, Targets, Timestamp
3643
from tuf.ngclient import Updater
44+
from tuf.ngclient.config import UpdaterConfig
3745

3846
from .exceptions import DigestMismatch, TargetNotFoundError
3947

40-
V2_REPOSITORY_URL = "https://agent-integration-wheels-prod.s3.amazonaws.com"
41-
4248
logger = logging.getLogger(__name__)
4349

50+
V2_REPOSITORY_URL = "https://agent-integration-wheels-prod.s3.amazonaws.com"
51+
4452

4553
class TUFPointerDownloader:
4654
"""Downloads Datadog integration wheels from a v2 TUF repository."""
@@ -87,48 +95,110 @@ def _make_updater(self, metadata_dir: Path, target_dir: Path) -> Updater:
8795
metadata_base_url=f'{self._repository_url}/metadata/',
8896
target_base_url=f'{self._repository_url}/targets/',
8997
target_dir=str(target_dir),
98+
config=UpdaterConfig(prefix_targets_with_hash=True),
9099
)
91100

101+
@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))
128+
129+
def _fetch_targets_unverified(self) -> Targets:
130+
"""Fetch targets metadata without verifying TUF signatures.
131+
132+
Walks timestamp → snapshot → targets to resolve the current versioned
133+
targets file and returns the parsed Targets signed payload.
134+
"""
135+
base = self._repository_url
136+
137+
with urllib.request.urlopen(f'{base}/metadata/timestamp.json') as r:
138+
ts: Metadata[Timestamp] = Metadata[Timestamp].from_bytes(r.read())
139+
snap_ver = ts.signed.snapshot_meta.version
140+
141+
with urllib.request.urlopen(f'{base}/metadata/{snap_ver}.snapshot.json') as r:
142+
snap: Metadata[Snapshot] = Metadata[Snapshot].from_bytes(r.read())
143+
tgt_ver = snap.signed.meta['targets.json'].version
144+
145+
with urllib.request.urlopen(f'{base}/metadata/{tgt_ver}.targets.json') as r:
146+
return Metadata[Targets].from_bytes(r.read()).signed
147+
92148
# ------------------------------------------------------------------
93149
# Public API
94150
# ------------------------------------------------------------------
95151

96152
def get_pointer(self, project: str, version: str | None = None) -> dict:
97-
"""Return the TUF-verified pointer JSON for *project* at *version*.
153+
"""Return the pointer JSON for *project* at *version*.
98154
99-
*version* = None resolves to ``latest.json`` (stable releases only).
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.
100158
Raises ``TargetNotFoundError`` if the target is absent from the TUF repo.
101159
"""
102-
target_path = f'{project}/{version or "latest"}.json'
103-
104160
with tempfile.TemporaryDirectory() as tmp:
105161
metadata_dir = Path(tmp) / 'metadata'
106162
target_dir = Path(tmp) / 'targets'
107163
metadata_dir.mkdir()
108164
target_dir.mkdir()
109165

110166
if self._disable_verification:
111-
url = f'{self._repository_url}/targets/{target_path}'
167+
targets = self._fetch_targets_unverified()
168+
resolved = self._resolve_version(project, version, targets)
169+
target_path = f'{project}/{resolved}.json'
170+
171+
entry = targets.targets.get(target_path)
172+
if entry is None:
173+
raise TargetNotFoundError(f'Target not found: {target_path}')
174+
175+
sha256 = entry.hashes.get('sha256', '')
176+
url = f'{self._repository_url}/targets/{project}/{sha256}.{resolved}.json'
112177
try:
113178
with urllib.request.urlopen(url) as resp:
114179
return json.loads(resp.read())
115180
except urllib.error.HTTPError as exc:
116181
if exc.code == 404:
117-
raise TargetNotFoundError(
118-
f'Pointer not found: {target_path}'
119-
) from exc
182+
raise TargetNotFoundError(f'Pointer not found: {url}') from exc
120183
raise
121184

122185
self._bootstrap_metadata_dir(metadata_dir)
123-
124186
updater = self._make_updater(metadata_dir, target_dir)
125187
updater.refresh()
126188

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+
127199
target_info = updater.get_targetinfo(target_path)
128200
if target_info is None:
129-
raise TargetNotFoundError(
130-
f'No TUF target for {project!r} version {version or "latest"!r}'
131-
)
201+
raise TargetNotFoundError(f'No TUF target for {project!r} version {resolved!r}')
132202

133203
pointer_path = target_dir / target_path
134204
pointer_path.parent.mkdir(parents=True, exist_ok=True)

0 commit comments

Comments
 (0)