Skip to content

Commit 813b2de

Browse files
jake11-ohoclaude
andcommitted
feat(admin): probe original namespace before mirror namespace in image registry mirror & bump version to 1.9.4 (#1161)
When the original image has a namespace (e.g. gcr.io/foo/python:3.11), probe registry-only replacement first (mirror.com/foo/python:3.11) before falling back to registry+namespace replacement (mirror.com/rock-public/python:3.11). Images without a namespace retain existing behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8c5e3c3 commit 813b2de

3 files changed

Lines changed: 72 additions & 35 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
77
authors = [{ name = "chatos@alibaba" }]
88
requires-python = "<4.0,>=3.10"
99
name = "rl-rock"
10-
version = "1.9.3"
10+
version = "1.9.4"
1111
description = "ROCK-Reinforcement Open Construction Kit"
1212
readme = "README.md"
1313
dependencies = [

rock/admin/entrypoints/sandbox_api.py

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,9 @@ async def _apply_image_registry_mirror(config: DockerDeploymentConfig) -> None:
207207

208208
_, repo_and_tag = ImageUtil.parse_registry_and_others(config.image)
209209
if "/" in repo_and_tag:
210-
_, name_tag = repo_and_tag.split("/", 1)
210+
original_namespace, name_tag = repo_and_tag.split("/", 1)
211211
else:
212+
original_namespace = None
212213
name_tag = repo_and_tag
213214
if "@" in name_tag:
214215
logger.info(
@@ -224,33 +225,38 @@ async def _apply_image_registry_mirror(config: DockerDeploymentConfig) -> None:
224225
for mirror in mirrors:
225226
if not mirror.registry or not mirror.namespace:
226227
continue
227-
candidate = f"{mirror.registry}/{mirror.namespace}/{name_tag}"
228-
229-
cached = _probe_cache_get(candidate)
230-
if cached is True:
231-
logger.info(f"image registry mirror hit (cached): {original_image!r} -> {candidate!r}")
232-
_apply_mirror_hit(config, mirror, candidate)
233-
return
234-
if cached is False:
235-
continue
236-
237-
try:
238-
hit = await _http_probe_manifest(
239-
registry=mirror.registry,
240-
repo=f"{mirror.namespace}/{image_name}",
241-
tag=tag,
242-
username=mirror.username,
243-
password=mirror.password,
244-
)
245-
except Exception as e:
246-
logger.warning(f"image registry mirror probe failed for {candidate!r}: {e}")
247-
continue
248228

249-
_probe_cache_set(candidate, hit)
250-
if hit:
251-
logger.info(f"image registry mirror hit: {original_image!r} -> {candidate!r}")
252-
_apply_mirror_hit(config, mirror, candidate)
253-
return
229+
candidates = []
230+
if original_namespace:
231+
candidates.append((f"{mirror.registry}/{original_namespace}/{name_tag}", f"{original_namespace}/{image_name}"))
232+
candidates.append((f"{mirror.registry}/{mirror.namespace}/{name_tag}", f"{mirror.namespace}/{image_name}"))
233+
234+
for candidate, repo in candidates:
235+
cached = _probe_cache_get(candidate)
236+
if cached is True:
237+
logger.info(f"image registry mirror hit (cached): {original_image!r} -> {candidate!r}")
238+
_apply_mirror_hit(config, mirror, candidate)
239+
return
240+
if cached is False:
241+
continue
242+
243+
try:
244+
hit = await _http_probe_manifest(
245+
registry=mirror.registry,
246+
repo=repo,
247+
tag=tag,
248+
username=mirror.username,
249+
password=mirror.password,
250+
)
251+
except Exception as e:
252+
logger.warning(f"image registry mirror probe failed for {candidate!r}: {e}")
253+
continue
254+
255+
_probe_cache_set(candidate, hit)
256+
if hit:
257+
logger.info(f"image registry mirror hit: {original_image!r} -> {candidate!r}")
258+
_apply_mirror_hit(config, mirror, candidate)
259+
return
254260
logger.info(f"image registry mirror miss for {original_image!r}, keep original")
255261

256262
async def _apply_timeout_defaults(config: DockerDeploymentConfig) -> None:

tests/unit/admin/test_image_registry_mirror.py

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,17 +87,37 @@ async def test_first_mirror_hit_rewrites_image(restore_sandbox_manager, stub_man
8787
]
8888
)
8989
config = DockerDeploymentConfig(image="gcr.io/foo/python:3.11")
90+
# First probe: registry-only (preserve original namespace "foo") -> hit
9091
stub_manifest_probe.probe_results.append(True)
9192

9293
await sandbox_api._apply_image_registry_mirror(config)
9394

94-
assert config.image == "rock-a.example.com/rock-public/python:3.11"
95+
assert config.image == "rock-a.example.com/foo/python:3.11"
9596
assert config.registry_username is None
9697
assert config.registry_password is None
9798
assert len(stub_manifest_probe.probes) == 1
99+
assert stub_manifest_probe.probes[0]["repo"] == "foo/python"
100+
101+
102+
async def test_registry_only_miss_falls_back_to_namespace_replace(restore_sandbox_manager, stub_manifest_probe):
103+
sandbox_api.sandbox_manager = _make_manager(
104+
[ImageRegistryMirror(registry="rock-a.example.com", namespace="rock-public")]
105+
)
106+
config = DockerDeploymentConfig(image="gcr.io/foo/python:3.11")
107+
# First probe: registry-only (foo/python) -> miss
108+
# Second probe: registry+namespace (rock-public/python) -> hit
109+
stub_manifest_probe.probe_results.extend([False, True])
110+
111+
await sandbox_api._apply_image_registry_mirror(config)
112+
113+
assert config.image == "rock-a.example.com/rock-public/python:3.11"
114+
assert len(stub_manifest_probe.probes) == 2
115+
assert stub_manifest_probe.probes[0]["repo"] == "foo/python"
116+
assert stub_manifest_probe.probes[1]["repo"] == "rock-public/python"
98117

99118

100119
async def test_second_mirror_hit_after_first_miss(restore_sandbox_manager, stub_manifest_probe):
120+
"""No original namespace (bare image) — only namespace-replace candidates are tried."""
101121
sandbox_api.sandbox_manager = _make_manager(
102122
[
103123
ImageRegistryMirror(registry="rock-a.example.com", namespace="rock-public"),
@@ -114,6 +134,7 @@ async def test_second_mirror_hit_after_first_miss(restore_sandbox_manager, stub_
114134

115135

116136
async def test_full_miss_keeps_original_image(restore_sandbox_manager, stub_manifest_probe):
137+
"""No original namespace — each mirror has 1 candidate, both miss."""
117138
sandbox_api.sandbox_manager = _make_manager(
118139
[
119140
ImageRegistryMirror(registry="rock-a.example.com", namespace="rock-public"),
@@ -208,17 +229,22 @@ async def test_invalid_mirror_entry_skipped(restore_sandbox_manager, stub_manife
208229

209230

210231
async def test_candidate_strips_registry_and_namespace_preserves_repo(restore_sandbox_manager, stub_manifest_probe):
232+
"""With original namespace 'project', first probe keeps it, second uses mirror namespace."""
211233
sandbox_api.sandbox_manager = _make_manager(
212234
[ImageRegistryMirror(registry="rock-a.example.com", namespace="rock-public")]
213235
)
214236
config = DockerDeploymentConfig(image="gcr.io/project/subdir/myimage:v1")
215-
stub_manifest_probe.probe_results.append(False)
237+
# First probe: registry-only (project/subdir/myimage) -> miss
238+
# Second probe: registry+namespace (rock-public/subdir/myimage) -> miss
239+
stub_manifest_probe.probe_results.extend([False, False])
216240

217241
await sandbox_api._apply_image_registry_mirror(config)
218242

219-
assert stub_manifest_probe.probes[0]["image"] == "rock-a.example.com/rock-public/subdir/myimage:v1"
220-
assert stub_manifest_probe.probes[0]["repo"] == "rock-public/subdir/myimage"
243+
assert stub_manifest_probe.probes[0]["image"] == "rock-a.example.com/project/subdir/myimage:v1"
244+
assert stub_manifest_probe.probes[0]["repo"] == "project/subdir/myimage"
221245
assert stub_manifest_probe.probes[0]["tag"] == "v1"
246+
assert stub_manifest_probe.probes[1]["image"] == "rock-a.example.com/rock-public/subdir/myimage:v1"
247+
assert stub_manifest_probe.probes[1]["repo"] == "rock-public/subdir/myimage"
222248

223249

224250
async def test_missing_tag_defaults_to_latest(restore_sandbox_manager, stub_manifest_probe):
@@ -371,12 +397,13 @@ async def test_wildcard_allowlist_lets_every_image_through(restore_sandbox_manag
371397
[ImageRegistryMirror(registry="rock-a.example.com", namespace="rock-public")],
372398
allowlist=["*"],
373399
)
400+
# First probe: registry-only (foo/python) -> hit
374401
stub_manifest_probe.probe_results.append(True)
375402
config = DockerDeploymentConfig(image="gcr.io/foo/python:3.11")
376403

377404
await sandbox_api._apply_image_registry_mirror(config)
378405

379-
assert config.image == "rock-a.example.com/rock-public/python:3.11"
406+
assert config.image == "rock-a.example.com/foo/python:3.11"
380407
assert len(stub_manifest_probe.probes) == 1
381408

382409

@@ -385,12 +412,16 @@ async def test_prefix_allowlist_matches_only_listed_images(restore_sandbox_manag
385412
[ImageRegistryMirror(registry="rock-a.example.com", namespace="rock-public")],
386413
allowlist=["aaaaaa/bbb/", "swe-bench:"],
387414
)
388-
stub_manifest_probe.probe_results.extend([True, True])
415+
# "aaaaaa/bbb/swe-bench:..." has original_namespace="aaaaaa":
416+
# 1st probe: registry-only (aaaaaa/bbb/swe-bench) -> hit
417+
stub_manifest_probe.probe_results.append(True)
389418

390419
allowed = DockerDeploymentConfig(image="aaaaaa/bbb/swe-bench:astropy__astropy-12907")
391420
await sandbox_api._apply_image_registry_mirror(allowed)
392-
assert allowed.image == "rock-a.example.com/rock-public/bbb/swe-bench:astropy__astropy-12907"
421+
assert allowed.image == "rock-a.example.com/aaaaaa/bbb/swe-bench:astropy__astropy-12907"
393422

423+
# "swe-bench:..." has no namespace -> only namespace-replace candidate
424+
stub_manifest_probe.probe_results.append(True)
394425
bare_prefix = DockerDeploymentConfig(image="swe-bench:python__python-1")
395426
await sandbox_api._apply_image_registry_mirror(bare_prefix)
396427
assert bare_prefix.image == "rock-a.example.com/rock-public/swe-bench:python__python-1"

0 commit comments

Comments
 (0)