Skip to content

Commit d241d68

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

4 files changed

Lines changed: 90 additions & 317 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add v2 TUF pointer downloader support.

datadog_checks_downloader/datadog_checks/downloader/cli.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,10 @@
88
import os
99
import re
1010
import sys
11-
from pathlib import Path
1211

1312
# 2nd party.
1413
from .download import DEFAULT_ROOT_LAYOUT_TYPE, REPOSITORY_URL_PREFIX, ROOT_LAYOUTS, TUFDownloader
15-
from .download_v2 import TUFPointerDownloader, V2_REPOSITORY_URL
14+
from .download_v2 import V2_REPOSITORY_URL, TUFPointerDownloader
1615
from .exceptions import NonCanonicalVersion, NonDatadogPackage
1716

1817
# Private module functions.
@@ -97,8 +96,6 @@ def instantiate_downloader():
9796
'-v', '--verbose', action='count', default=0, help='Show verbose information about TUF and in-toto.'
9897
)
9998

100-
101-
10299
args = parser.parse_args()
103100
repository_url_prefix = args.repository
104101
standard_distribution_name = args.standard_distribution_name
@@ -160,9 +157,8 @@ def download():
160157
_download_v2()
161158
except Exception as exc:
162159
import logging
163-
logging.getLogger(__name__).info(
164-
'v2 download failed (%s: %s), falling back to v1', type(exc).__name__, exc
165-
)
160+
161+
logging.getLogger(__name__).info('v2 download failed (%s: %s), falling back to v1', type(exc).__name__, exc)
166162
tuf_downloader, standard_distribution_name, version, ignore_python_version = instantiate_downloader()
167163
run_downloader(tuf_downloader, standard_distribution_name, version, ignore_python_version)
168164

@@ -176,12 +172,14 @@ def _download_v2():
176172
type=str,
177173
help='Standard distribution name of the desired Datadog check, e.g. datadog-postgres.',
178174
)
179-
parser.add_argument('--repository', type=str, default=V2_REPOSITORY_URL, help='HTTPS base URL of the v2 TUF repository.')
175+
parser.add_argument(
176+
'--repository', type=str, default=V2_REPOSITORY_URL, help='HTTPS base URL of the v2 TUF repository.'
177+
)
180178
parser.add_argument('--version', type=str, default=None, help='Version to download (default: latest stable).')
181179
parser.add_argument(
182180
'--unsafe-disable-verification',
183181
action='store_true',
184-
help='Disable TUF verification and wheel digest checks.',
182+
help='Disable TUF verification and wheel digest checks; requires --version and downloads /wheels directly.',
185183
)
186184
parser.add_argument('-v', '--verbose', action='count', default=0)
187185
parser.add_argument('--v2', action='store_true', default=True) # consumed upstream; kept for completeness
@@ -207,8 +205,7 @@ def _download_v2():
207205
sys.stderr.write('WARNING: --type is not applicable with --v2 and will be ignored.\n')
208206
if args._ignore_python_version:
209207
sys.stderr.write(
210-
'NOTE: --ignore-python-version is not applicable with --v2 '
211-
'(wheel selection happens at publish time).\n'
208+
'NOTE: --ignore-python-version is not applicable with --v2 (wheel selection happens at publish time).\n'
212209
)
213210

214211
downloader = TUFPointerDownloader(

datadog_checks_downloader/datadog_checks/downloader/download_v2.py

Lines changed: 34 additions & 94 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
@@ -34,12 +35,9 @@
3435
import json
3536
import logging
3637
import tempfile
37-
import urllib.error
3838
import urllib.request
3939
from pathlib import Path
4040

41-
from packaging.version import InvalidVersion, Version
42-
from tuf.api.metadata import Metadata, Snapshot, Targets, Timestamp
4341
from tuf.ngclient import Updater
4442
from tuf.ngclient.config import UpdaterConfig
4543

@@ -74,10 +72,7 @@ def __init__(
7472
self._disable_verification = disable_verification
7573

7674
if disable_verification:
77-
logger.warning(
78-
'Running with TUF verification disabled. '
79-
'Integrity is protected only by TLS (HTTPS).'
80-
)
75+
logger.warning('Running with TUF verification disabled. Integrity is protected only by TLS (HTTPS).')
8176

8277
# ------------------------------------------------------------------
8378
# Internal helpers
@@ -99,51 +94,17 @@ def _make_updater(self, metadata_dir: Path, target_dir: Path) -> Updater:
9994
)
10095

10196
@staticmethod
102-
def _resolve_version(project: str, version: str | None, targets: Targets) -> str:
103-
"""Return the concrete version to fetch.
97+
def _target_path(project: str, version: str | None) -> str:
98+
name = version if version is not None else 'latest'
99+
return f'{project}/{name}.json'
104100

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
101+
@staticmethod
102+
def _wheel_filename(project: str, version: str) -> str:
103+
distribution = project.replace('-', '_')
104+
return f'{distribution}-{version}-py3-none-any.whl'
144105

145-
with urllib.request.urlopen(f'{base}/metadata/{tgt_ver}.targets.json') as r:
146-
return Metadata[Targets].from_bytes(r.read()).signed
106+
def _direct_wheel_url(self, project: str, version: str) -> str:
107+
return f'{self._repository_url}/wheels/{project}/{self._wheel_filename(project, version)}'
147108

148109
# ------------------------------------------------------------------
149110
# Public API
@@ -152,9 +113,7 @@ def _fetch_targets_unverified(self) -> Targets:
152113
def get_pointer(self, project: str, version: str | None = None) -> dict:
153114
"""Return the pointer JSON for *project* at *version*.
154115
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.
116+
Resolves *version* = None by fetching ``<project>/latest.json``.
158117
Raises ``TargetNotFoundError`` if the target is absent from the TUF repo.
159118
"""
160119
with tempfile.TemporaryDirectory() as tmp:
@@ -163,42 +122,15 @@ def get_pointer(self, project: str, version: str | None = None) -> dict:
163122
metadata_dir.mkdir()
164123
target_dir.mkdir()
165124

166-
if self._disable_verification:
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'
177-
try:
178-
with urllib.request.urlopen(url) as resp:
179-
return json.loads(resp.read())
180-
except urllib.error.HTTPError as exc:
181-
if exc.code == 404:
182-
raise TargetNotFoundError(f'Pointer not found: {url}') from exc
183-
raise
184-
125+
target_path = self._target_path(project, version)
185126
self._bootstrap_metadata_dir(metadata_dir)
186127
updater = self._make_updater(metadata_dir, target_dir)
187128
updater.refresh()
188129

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-
199130
target_info = updater.get_targetinfo(target_path)
200131
if target_info is None:
201-
raise TargetNotFoundError(f'No TUF target for {project!r} version {resolved!r}')
132+
label = version if version is not None else 'latest stable'
133+
raise TargetNotFoundError(f'No TUF target for {project!r} version {label!r}')
202134

203135
pointer_path = target_dir / target_path
204136
pointer_path.parent.mkdir(parents=True, exist_ok=True)
@@ -218,20 +150,28 @@ def download(
218150
Raises ``DigestMismatch`` if the wheel digest or length does not match
219151
the pointer file.
220152
"""
221-
pointer = self.get_pointer(project, version)
222-
# Use the caller-supplied repository URL for the wheel fetch so that
223-
# --repository can point at a staging bucket and override the prod URL
224-
# baked into the pointer file. This allows pre-promotion validation
225-
# against staging S3 without modifying pointer content.
226-
wheel_url = self._repository_url + pointer['wheel_path']
227-
wheel_filename = Path(pointer['wheel_path']).name
153+
if self._disable_verification:
154+
if version is None:
155+
raise TargetNotFoundError('unsafe-disable-verification requires an explicit --version')
156+
wheel_url = self._direct_wheel_url(project, version)
157+
wheel_filename = self._wheel_filename(project, version)
158+
pointer = None
159+
else:
160+
pointer = self.get_pointer(project, version)
161+
# Use the caller-supplied repository URL for the wheel fetch so that
162+
# --repository can point at a staging bucket and override the prod URL
163+
# baked into the pointer file. This allows pre-promotion validation
164+
# against staging S3 without modifying pointer content.
165+
wheel_url = self._repository_url + pointer['wheel_path']
166+
wheel_filename = Path(pointer['wheel_path']).name
167+
228168
dest = (dest_dir or Path(tempfile.mkdtemp())) / wheel_filename
229169

230170
logger.info('Downloading wheel from %s', wheel_url)
231171
with urllib.request.urlopen(wheel_url) as resp:
232172
content = resp.read()
233173

234-
if not self._disable_verification:
174+
if pointer is not None:
235175
actual_digest = hashlib.sha256(content).hexdigest()
236176
if actual_digest != pointer['digest']:
237177
raise DigestMismatch(project, pointer['digest'], actual_digest)

0 commit comments

Comments
 (0)