Skip to content

Commit 13a00e9

Browse files
Issac-Newtonclaude
andcommitted
refactor(sdk): decouple _ImageResolver builder lifecycle and sync push creds
- Split _ImageResolver into create_builder() / resolve() / resolve_with_builder() so callers (notably tests) can manage the builder sandbox themselves and inject environment-specific setup (e.g. iptables NAT) before the build runs. The original resolve() now reliably stops the builder in a finally block. - Sandbox.start() now syncs Image.registry_{username,password} to the config so admin gets the credentials it needs to pull the image we just pushed. - Default ROCK_IMAGE_BUILDER_IMAGE bumped to :latest so all instances pick up the builder image with bip=192.168.250.0/24 (avoids 172.17/16 collision with admin's docker0) and insecure-registries pre-configured for local test registries. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent f3129fe commit 13a00e9

3 files changed

Lines changed: 54 additions & 26 deletions

File tree

rock/env_vars.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888
"ROCK_ENVHUB_DEFAULT_DOCKER_IMAGE": lambda: os.getenv("ROCK_ENVHUB_DEFAULT_DOCKER_IMAGE", "python:3.11"),
8989
"ROCK_IMAGE_BUILDER_IMAGE": lambda: os.getenv(
9090
"ROCK_IMAGE_BUILDER_IMAGE",
91-
"rock-n-roll-registry.cn-hangzhou.cr.aliyuncs.com/rock/rock-env-builder:0.2.1a1",
91+
"rock-n-roll-registry.cn-hangzhou.cr.aliyuncs.com/rock/rock-env-builder:latest",
9292
),
9393
"ROCK_ENVHUB_DB_URL": lambda: os.getenv(
9494
"ROCK_ENVHUB_DB_URL", f"sqlite:///{Path.home() / '.rock' / 'rock_envs.db'}"

rock/sdk/sandbox/client.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,11 @@ async def start(self):
175175
cluster=self.config.cluster,
176176
extra_headers=self.config.extra_headers,
177177
)
178+
# Sync image's registry credentials to SandboxConfig so admin can pull the
179+
# built image. Don't override caller-provided creds.
180+
if image_obj.registry_username and not self.config.registry_username:
181+
self.config.registry_username = image_obj.registry_username
182+
self.config.registry_password = image_obj.registry_password
178183

179184
url = f"{self._url}/start_async"
180185
headers = self._build_headers()

rock/sdk/sandbox/image_resolver.py

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ class _ImageResolver:
9595
对于 dockerfile image,启动一个 builder sandbox 完成 DinD 构建和推送。
9696
"""
9797

98+
BUILD_SESSION = "build"
99+
98100
def __init__(
99101
self,
100102
*,
@@ -110,10 +112,12 @@ def __init__(
110112
self._builder_image = builder_image
111113
self._sandbox_factory = _sandbox_factory
112114

113-
async def resolve(self, image: Image) -> str:
114-
if not image.needs_build:
115-
return image.image_name
115+
def create_builder(self) -> Sandbox:
116+
"""Construct (but do not start) the builder sandbox.
116117
118+
Exposed so callers can start, customise (e.g. inject test-only NAT rules), then
119+
hand the running builder to :meth:`resolve_with_builder`.
120+
"""
117121
builder_image = self._builder_image or env_vars.ROCK_IMAGE_BUILDER_IMAGE
118122
builder_cfg = SandboxConfig(
119123
image=builder_image,
@@ -124,36 +128,55 @@ async def resolve(self, image: Image) -> str:
124128
auto_clear_seconds=60 * 30,
125129
)
126130
factory = self._sandbox_factory or Sandbox
127-
builder = factory(builder_cfg)
128-
session = "build"
129-
try:
130-
await builder.start()
131-
await builder.create_session(CreateBashSessionRequest(session=session))
131+
return factory(builder_cfg)
132132

133-
# ── Phase 1: Start dockerd ──
134-
await self._run_script(builder, session, _DOCKERD_SCRIPT, "/tmp/rock_dockerd.sh", "DOCKERD_OK", 120)
135-
136-
# ── Phase 2: Build image ──
137-
content_hash = image.content_hash()
138-
context_path = await self._upload_context(builder, session, image)
139-
build_script = self._gen_build_script(image, content_hash, context_path)
140-
build_output = await self._run_script(builder, session, build_script, "/tmp/rock_build.sh", "BUILD_OK", 600)
141-
if "CACHE_HIT" in build_output:
142-
logger.info("Image %s cache hit, skipping push", image.image_name)
143-
return image.image_name
144-
145-
# ── Phase 3: Login and push ──
146-
push_script = self._gen_push_script(image)
147-
await self._run_script(builder, session, push_script, "/tmp/rock_push.sh", "PUSH_OK", 300)
148-
149-
logger.info("Successfully built and pushed image %s", image.image_name)
133+
async def resolve(self, image: Image) -> str:
134+
"""Resolve `image` by managing the builder lifecycle internally."""
135+
if not image.needs_build:
150136
return image.image_name
137+
138+
builder = self.create_builder()
139+
try:
140+
await builder.start()
141+
return await self.resolve_with_builder(image, builder)
151142
finally:
152143
try:
153144
await builder.stop()
154145
except Exception:
155146
logger.warning("Failed to stop builder sandbox: %s", builder.sandbox_id, exc_info=True)
156147

148+
async def resolve_with_builder(self, image: Image, builder: Sandbox) -> str:
149+
"""Run the build/push pipeline against an externally-managed, already-started
150+
builder sandbox.
151+
152+
The caller owns `builder`'s lifecycle (start/stop) and is free to perform any
153+
environment-specific setup (firewall rules, mounts, etc.) before calling this.
154+
"""
155+
if not image.needs_build:
156+
return image.image_name
157+
158+
session = self.BUILD_SESSION
159+
await builder.create_session(CreateBashSessionRequest(session=session))
160+
161+
# ── Phase 1: Start dockerd ──
162+
await self._run_script(builder, session, _DOCKERD_SCRIPT, "/tmp/rock_dockerd.sh", "DOCKERD_OK", 120)
163+
164+
# ── Phase 2: Build image ──
165+
content_hash = image.content_hash()
166+
context_path = await self._upload_context(builder, session, image)
167+
build_script = self._gen_build_script(image, content_hash, context_path)
168+
build_output = await self._run_script(builder, session, build_script, "/tmp/rock_build.sh", "BUILD_OK", 600)
169+
if "CACHE_HIT" in build_output:
170+
logger.info("Image %s cache hit, skipping push", image.image_name)
171+
return image.image_name
172+
173+
# ── Phase 3: Login and push ──
174+
push_script = self._gen_push_script(image)
175+
await self._run_script(builder, session, push_script, "/tmp/rock_push.sh", "PUSH_OK", 300)
176+
177+
logger.info("Successfully built and pushed image %s", image.image_name)
178+
return image.image_name
179+
157180
async def _run_script(
158181
self, builder, session: str, script: str, remote_path: str, success_marker: str, timeout: int
159182
) -> str:

0 commit comments

Comments
 (0)