Skip to content

Commit 2344842

Browse files
jake11-ohoclaude
andauthored
feat(sandbox): inject INSTANCE_ROCK_REGISTRY env var into launched sandboxes (#1090)
Add RuntimeConfig.instance_registry_mirrors (configurable via YAML and Nacos hot-reload). Docker, Ray, and K8s backends inject INSTANCE_ROCK_REGISTRY=<comma-joined> into every launched container. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9075819 commit 2344842

5 files changed

Lines changed: 238 additions & 11 deletions

File tree

rock-conf/rock-local.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ warmup:
2424
# - "swe-bench:" # bare image name prefix
2525
# - "aaaaaa/bbb/" # registry/namespace prefix
2626

27+
# Inject INSTANCE_ROCK_REGISTRY into every launched sandbox so the
28+
# runtime can rewrite prebuilt images to a ROCK mirror first.
29+
# Configured via YAML or Nacos (hot-reload supported).
30+
# Nacos YAML format:
31+
#
32+
# runtime:
33+
# instance_registry_mirrors:
34+
# - "reg-a.aliyuncs.com/mirror-1"
35+
# - "reg-b.aliyuncs.com/mirror-2"
36+
2737
# Scheduler configuration
2838
scheduler:
2939
enabled: true # Whether to enable the scheduler

rock/config.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,12 @@ class RuntimeConfig:
274274
sandbox_disk_limit_rootfs: str | None = None
275275
"""Default rootfs quota per container. None means no limit. Can be overridden by nacos key 'default_disk_limit'."""
276276

277+
instance_registry_mirrors: list[str] = field(default_factory=list)
278+
"""Registry mirrors injected into each launched sandbox as
279+
INSTANCE_ROCK_REGISTRY=<comma-joined>. Each entry is a host/namespace
280+
string (e.g. "reg-a.aliyuncs.com/mirror-1"). When empty, the env var is
281+
not set and downstream tools skip mirror rewriting."""
282+
277283
def __post_init__(self) -> None:
278284
# Convert dict to StandardSpec if needed
279285
if isinstance(self.standard_spec, dict):
@@ -511,8 +517,14 @@ async def update(self):
511517
if "image_mirror_lookup_allowlist" in nacos_result:
512518
self.image_mirror_lookup_allowlist = list(nacos_result["image_mirror_lookup_allowlist"] or [])
513519

520+
if "runtime" in nacos_result and isinstance(nacos_result["runtime"], dict):
521+
runtime_overrides = nacos_result["runtime"]
522+
if "instance_registry_mirrors" in runtime_overrides:
523+
self.runtime.instance_registry_mirrors = list(runtime_overrides["instance_registry_mirrors"] or [])
524+
514525
logger.info(
515526
f"Updated config from Nacos: sandbox_config={self.sandbox_config}, proxy_service={self.proxy_service}"
516527
f", image_registry_mirrors={self.image_registry_mirrors}"
517528
f", image_mirror_lookup_allowlist={self.image_mirror_lookup_allowlist}"
529+
f", instance_registry_mirrors={self.runtime.instance_registry_mirrors}"
518530
)

rock/deployments/docker.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -523,8 +523,6 @@ async def start(self):
523523
if self._config.remove_container:
524524
rm_arg = ["--rm"]
525525

526-
env_arg = []
527-
528526
# Conditionally set up logging path mount based on ROCK_LOGGING_PATH.
529527
log_file_path: str | None = None
530528
volume_args = self._prepare_volume_mounts()
@@ -533,23 +531,17 @@ async def start(self):
533531
os.makedirs(log_file_path, exist_ok=True)
534532
os.chmod(log_file_path, 0o777)
535533
volume_args.extend(["-v", f"{log_file_path}:{env_vars.ROCK_LOGGING_PATH}"])
536-
env_arg = [
537-
"-e",
538-
f"ROCK_LOGGING_PATH={env_vars.ROCK_LOGGING_PATH}",
539-
"-e",
540-
f"ROCK_LOGGING_LEVEL={env_vars.ROCK_LOGGING_LEVEL}",
541-
]
542534

543-
env_arg.extend(["-e", f"ROCK_TIME_ZONE={env_vars.ROCK_TIME_ZONE}"])
544535
volume_args.extend(self._prepare_timezone_mount())
545536

546-
# Kata DinD: prepare disk image and add volume mount + env var
537+
# Kata DinD: prepare disk image and add volume mount
547538
if self._config.use_kata_runtime:
548539
with StageTimer("startup_timing", f"[{self._container_name}] Kata disk prepare", logger):
549540
self._prepare_kata_disk()
550541
disk_path = self._get_kata_disk_image_path()
551542
volume_args.extend(["-v", f"{disk_path}:/docker-disk.img"])
552-
env_arg.extend(["-e", "ROCK_KATA_RUNTIME=true"])
543+
544+
env_arg = self._build_env_args()
553545

554546
with StageTimer("startup_timing", f"[{self._container_name}] Random sleep", logger):
555547
time.sleep(random.randint(0, 5))
@@ -611,6 +603,32 @@ async def start(self):
611603
if self._config.enable_auto_clear:
612604
self._check_stop_task = asyncio.create_task(self._check_stop())
613605

606+
def _build_env_args(self) -> list[str]:
607+
"""Construct `-e KEY=VALUE` argv pairs for `docker run`.
608+
609+
Centralizes every env var rock injects into a sandbox in one place so
610+
callers (and tests) can audit them together. Volume mounts that pair
611+
with these vars (ROCK_LOGGING_PATH, kata disk) are still set up
612+
alongside in `start()`.
613+
"""
614+
args: list[str] = []
615+
if env_vars.ROCK_LOGGING_PATH:
616+
args.extend(
617+
[
618+
"-e",
619+
f"ROCK_LOGGING_PATH={env_vars.ROCK_LOGGING_PATH}",
620+
"-e",
621+
f"ROCK_LOGGING_LEVEL={env_vars.ROCK_LOGGING_LEVEL}",
622+
]
623+
)
624+
args.extend(["-e", f"ROCK_TIME_ZONE={env_vars.ROCK_TIME_ZONE}"])
625+
if self._config.use_kata_runtime:
626+
args.extend(["-e", "ROCK_KATA_RUNTIME=true"])
627+
mirrors = self._config.runtime_config.instance_registry_mirrors
628+
if mirrors:
629+
args.extend(["-e", f"INSTANCE_ROCK_REGISTRY={','.join(mirrors)}"])
630+
return args
631+
614632
def _prepare_timezone_mount(self) -> list[str]:
615633
tz = env_vars.ROCK_TIME_ZONE
616634
localtime_src = f"/usr/share/zoneinfo/{tz}"
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""Unit tests for DockerDeployment._build_env_args.
2+
3+
Covers the env-arg list assembled for `docker run -e ...`, focusing on the
4+
INSTANCE_ROCK_REGISTRY injection sourced from RuntimeConfig.instance_registry_mirrors.
5+
Existing entries (ROCK_LOGGING_*, ROCK_TIME_ZONE, ROCK_KATA_RUNTIME) are regression-checked.
6+
"""
7+
8+
from unittest.mock import patch
9+
10+
from rock.config import RuntimeConfig
11+
from rock.deployments.config import DockerDeploymentConfig
12+
from rock.deployments.docker import DockerDeployment
13+
14+
15+
def _make_deployment(**config_kwargs) -> DockerDeployment:
16+
with patch("rock.deployments.docker.DockerSandboxValidator"):
17+
return DockerDeployment.from_config(DockerDeploymentConfig(**config_kwargs))
18+
19+
20+
class TestBuildEnvArgsInstanceRegistry:
21+
def test_no_mirrors_means_no_registry_env(self):
22+
deployment = _make_deployment()
23+
with patch("rock.deployments.docker.env_vars") as mock_env:
24+
mock_env.ROCK_LOGGING_PATH = ""
25+
mock_env.ROCK_TIME_ZONE = "UTC"
26+
args = deployment._build_env_args()
27+
assert "INSTANCE_ROCK_REGISTRY" not in " ".join(args)
28+
29+
def test_single_mirror_emitted_unquoted(self):
30+
deployment = _make_deployment(
31+
runtime_config=RuntimeConfig(instance_registry_mirrors=["reg-a.example.com/ns"]),
32+
)
33+
with patch("rock.deployments.docker.env_vars") as mock_env:
34+
mock_env.ROCK_LOGGING_PATH = ""
35+
mock_env.ROCK_TIME_ZONE = "UTC"
36+
args = deployment._build_env_args()
37+
assert "-e" in args
38+
assert "INSTANCE_ROCK_REGISTRY=reg-a.example.com/ns" in args
39+
40+
def test_multiple_mirrors_comma_joined(self):
41+
deployment = _make_deployment(
42+
runtime_config=RuntimeConfig(
43+
instance_registry_mirrors=[
44+
"reg-a.example.com/ns-1",
45+
"reg-b.example.com/ns-2",
46+
],
47+
),
48+
)
49+
with patch("rock.deployments.docker.env_vars") as mock_env:
50+
mock_env.ROCK_LOGGING_PATH = ""
51+
mock_env.ROCK_TIME_ZONE = "UTC"
52+
args = deployment._build_env_args()
53+
assert "INSTANCE_ROCK_REGISTRY=reg-a.example.com/ns-1,reg-b.example.com/ns-2" in args
54+
55+
56+
class TestBuildEnvArgsRegression:
57+
"""The existing env entries must keep working after the extraction."""
58+
59+
def test_timezone_always_present(self):
60+
deployment = _make_deployment()
61+
with patch("rock.deployments.docker.env_vars") as mock_env:
62+
mock_env.ROCK_LOGGING_PATH = ""
63+
mock_env.ROCK_TIME_ZONE = "Asia/Shanghai"
64+
args = deployment._build_env_args()
65+
assert "ROCK_TIME_ZONE=Asia/Shanghai" in args
66+
67+
def test_logging_entries_present_when_logging_path_set(self):
68+
deployment = _make_deployment()
69+
with patch("rock.deployments.docker.env_vars") as mock_env:
70+
mock_env.ROCK_LOGGING_PATH = "/var/log/rock"
71+
mock_env.ROCK_LOGGING_LEVEL = "INFO"
72+
mock_env.ROCK_TIME_ZONE = "UTC"
73+
args = deployment._build_env_args()
74+
assert "ROCK_LOGGING_PATH=/var/log/rock" in args
75+
assert "ROCK_LOGGING_LEVEL=INFO" in args
76+
77+
def test_logging_entries_absent_when_logging_path_empty(self):
78+
deployment = _make_deployment()
79+
with patch("rock.deployments.docker.env_vars") as mock_env:
80+
mock_env.ROCK_LOGGING_PATH = ""
81+
mock_env.ROCK_TIME_ZONE = "UTC"
82+
args = deployment._build_env_args()
83+
joined = " ".join(args)
84+
assert "ROCK_LOGGING_PATH" not in joined
85+
assert "ROCK_LOGGING_LEVEL" not in joined
86+
87+
def test_kata_runtime_env_present_when_enabled(self):
88+
deployment = _make_deployment(use_kata_runtime=True)
89+
with patch("rock.deployments.docker.env_vars") as mock_env:
90+
mock_env.ROCK_LOGGING_PATH = ""
91+
mock_env.ROCK_TIME_ZONE = "UTC"
92+
args = deployment._build_env_args()
93+
assert "ROCK_KATA_RUNTIME=true" in args
94+
95+
def test_kata_runtime_env_absent_when_disabled(self):
96+
deployment = _make_deployment(use_kata_runtime=False)
97+
with patch("rock.deployments.docker.env_vars") as mock_env:
98+
mock_env.ROCK_LOGGING_PATH = ""
99+
mock_env.ROCK_TIME_ZONE = "UTC"
100+
args = deployment._build_env_args()
101+
assert "ROCK_KATA_RUNTIME=true" not in args

tests/unit/test_config.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,42 @@ async def test_runtime_config():
5656
assert runtime_config.standard_spec.cpus == 2
5757

5858

59+
def test_runtime_config_instance_registry_mirrors_default_empty():
60+
cfg = RuntimeConfig()
61+
assert cfg.instance_registry_mirrors == []
62+
63+
64+
def test_runtime_config_instance_registry_mirrors_accepts_list():
65+
mirrors = ["reg-a.example.com/ns-1", "reg-b.example.com/ns-2"]
66+
cfg = RuntimeConfig(instance_registry_mirrors=mirrors)
67+
assert cfg.instance_registry_mirrors == mirrors
68+
69+
70+
@pytest.mark.asyncio
71+
async def test_runtime_config_instance_registry_mirrors_from_yaml(tmp_path, monkeypatch):
72+
yaml_path = tmp_path / "rock-test.yml"
73+
yaml_path.write_text(
74+
yaml.safe_dump(
75+
{
76+
"runtime": {
77+
"instance_registry_mirrors": [
78+
"reg-a.example.com/mirror-1",
79+
"reg-b.example.com/mirror-2",
80+
],
81+
},
82+
}
83+
)
84+
)
85+
# RuntimeConfig.__post_init__ requires ROCK_PYTHON_ENV_PATH + ROCK_ENVHUB_DB_URL.
86+
monkeypatch.setenv("ROCK_PYTHON_ENV_PATH", "/usr")
87+
monkeypatch.setenv("ROCK_ENVHUB_DB_URL", "sqlite:////tmp/test.db")
88+
rock_config = RockConfig.from_env(str(yaml_path))
89+
assert rock_config.runtime.instance_registry_mirrors == [
90+
"reg-a.example.com/mirror-1",
91+
"reg-b.example.com/mirror-2",
92+
]
93+
94+
5995
def test_oss_config_defaults():
6096
from rock.config import OssAccountConfig, OssConfig
6197

@@ -214,6 +250,56 @@ async def test_nacos_update_without_mirror_keys_preserves_existing():
214250
assert rock_config.image_mirror_lookup_allowlist == ["*"]
215251

216252

253+
# ===== Nacos hot-reload for instance_registry_mirrors =====
254+
255+
256+
@pytest.mark.asyncio
257+
async def test_nacos_update_loads_instance_registry_mirrors():
258+
rock_config = RockConfig()
259+
rock_config.nacos_provider = MagicMock()
260+
rock_config.nacos_provider.get_config = AsyncMock(
261+
return_value={
262+
"runtime": {
263+
"instance_registry_mirrors": ["reg-a.example.com/ns-1", "reg-b.example.com/ns-2"],
264+
},
265+
}
266+
)
267+
268+
await rock_config.update()
269+
270+
assert rock_config.runtime.instance_registry_mirrors == ["reg-a.example.com/ns-1", "reg-b.example.com/ns-2"]
271+
272+
273+
@pytest.mark.asyncio
274+
async def test_nacos_update_empty_instance_mirrors_clears_list():
275+
rock_config = RockConfig()
276+
rock_config.runtime.instance_registry_mirrors = ["old.example.com/ns"]
277+
rock_config.nacos_provider = MagicMock()
278+
rock_config.nacos_provider.get_config = AsyncMock(
279+
return_value={
280+
"runtime": {
281+
"instance_registry_mirrors": [],
282+
},
283+
}
284+
)
285+
286+
await rock_config.update()
287+
288+
assert rock_config.runtime.instance_registry_mirrors == []
289+
290+
291+
@pytest.mark.asyncio
292+
async def test_nacos_update_without_runtime_key_preserves_instance_mirrors():
293+
rock_config = RockConfig()
294+
rock_config.runtime.instance_registry_mirrors = ["keep.example.com/ns"]
295+
rock_config.nacos_provider = MagicMock()
296+
rock_config.nacos_provider.get_config = AsyncMock(return_value={"sandbox_config": {}})
297+
298+
await rock_config.update()
299+
300+
assert rock_config.runtime.instance_registry_mirrors == ["keep.example.com/ns"]
301+
302+
217303
# ===== _resolve_k8s_template_includes =====
218304

219305

0 commit comments

Comments
 (0)