Skip to content

Commit 0a88ac5

Browse files
committed
support docker login
1 parent e8e04a2 commit 0a88ac5

File tree

7 files changed

+90
-0
lines changed

7 files changed

+90
-0
lines changed

rock/admin/proto/request.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ class SandboxStartRequest(BaseModel):
2727
"""The amount of CPUs to allocate for the container."""
2828
sandbox_id: str | None = Field(default=None)
2929
"""The id of the sandbox."""
30+
registry_username: str | None = None
31+
"""Username for Docker registry authentication. When both username and password are provided, docker login will be performed before pulling the image."""
32+
registry_password: str | None = None
33+
"""Password for Docker registry authentication. When both username and password are provided, docker login will be performed before pulling the image."""
3034

3135

3236
class SandboxCommand(Command):

rock/deployments/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ class DockerDeploymentConfig(DeploymentConfig):
109109
actor_resource_num: float = 1
110110
"""Number of actor resources to allocate (to be refined)."""
111111

112+
registry_username: str | None = None
113+
"""Username for Docker registry authentication. When both username and password are provided, docker login will be performed before pulling the image."""
114+
115+
registry_password: str | None = None
116+
"""Password for Docker registry authentication. When both username and password are provided, docker login will be performed before pulling the image."""
117+
112118
runtime_config: RuntimeConfig = Field(default_factory=RuntimeConfig)
113119
"""Runtime configuration settings."""
114120

rock/deployments/docker.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from rock.deployments.config import DockerDeploymentConfig
1919
from rock.deployments.constants import Port, Status
2020
from rock.deployments.hooks.abstract import CombinedDeploymentHook, DeploymentHook
21+
from rock.deployments.hooks.docker_login import DockerLoginHook
2122
from rock.deployments.runtime_env import DockerRuntimeEnv, LocalRuntimeEnv, PipRuntimeEnv, UvRuntimeEnv
2223
from rock.deployments.sandbox_validator import DockerSandboxValidator
2324
from rock.deployments.status import PersistedServiceStatus, ServiceStatus
@@ -76,6 +77,11 @@ def __init__(
7677
else:
7778
raise Exception(f"Invalid ROCK_WORKER_ENV_TYPE: {env_vars.ROCK_WORKER_ENV_TYPE}")
7879

80+
if self._config.registry_username is not None and self._config.registry_password is not None:
81+
self.add_hook(
82+
DockerLoginHook(self._config.image, self._config.registry_username, self._config.registry_password)
83+
)
84+
7985
self.sandbox_validator: DockerSandboxValidator | None = DockerSandboxValidator()
8086

8187
def add_hook(self, hook: DeploymentHook):
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import asyncio
2+
3+
from rock.deployments.hooks.abstract import DeploymentHook
4+
from rock.logger import init_logger
5+
from rock.utils import DockerUtil, ImageUtil
6+
7+
logger = init_logger(__name__)
8+
9+
10+
class DockerLoginHook(DeploymentHook):
11+
"""Hook that performs Docker registry authentication before pulling images.
12+
13+
When triggered by the "Pulling docker image" step, this hook parses the
14+
registry from the image name and logs in using the provided credentials.
15+
"""
16+
17+
_PULL_STEP_MESSAGE = "Pulling docker image"
18+
19+
def __init__(self, image: str, username: str, password: str):
20+
self._image = image
21+
self._username = username
22+
self._password = password
23+
24+
def on_custom_step(self, message: str):
25+
if message != self._PULL_STEP_MESSAGE:
26+
return
27+
28+
loop = asyncio.new_event_loop()
29+
try:
30+
registry, _ = loop.run_until_complete(ImageUtil.parse_registry_and_others(self._image))
31+
finally:
32+
loop.close()
33+
if registry:
34+
logger.info(f"Authenticating to registry {registry!r} before pulling image")
35+
DockerUtil.login(registry, self._username, self._password)
36+
else:
37+
logger.warning(f"No registry found in image name {self._image!r}, skipping docker login")

rock/sdk/sandbox/client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ async def start(self):
171171
"startup_timeout": self.config.startup_timeout,
172172
"memory": self.config.memory,
173173
"cpus": self.config.cpus,
174+
"registry_username": self.config.registry_username,
175+
"registry_password": self.config.registry_password,
174176
}
175177
try:
176178
response = await HttpUtils.post(url, headers, data)

rock/sdk/sandbox/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ class SandboxConfig(BaseConfig):
3939
experiment_id: str | None = None
4040
cluster: str = "zb"
4141
namespace: str | None = None
42+
registry_username: str | None = None
43+
registry_password: str | None = None
4244

4345

4446
class SandboxGroupConfig(SandboxConfig):

rock/utils/docker.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,39 @@ def pull_image(cls, image: str) -> bytes:
3636
# e.stderr contains the error message as bytes
3737
raise subprocess.CalledProcessError(e.returncode, e.cmd, e.output, e.stderr) from None
3838

39+
@classmethod
40+
def login(cls, registry: str, username: str, password: str, timeout: int = 30) -> str:
41+
"""Login to a Docker registry
42+
43+
Args:
44+
registry: Docker registry URL (e.g. registry.example.com)
45+
username: Registry username
46+
password: Registry password
47+
timeout: Command timeout in seconds
48+
49+
Returns:
50+
Command output as string on success
51+
52+
Raises:
53+
subprocess.CalledProcessError: If login fails
54+
"""
55+
try:
56+
result = subprocess.run(
57+
["docker", "login", registry, "-u", username, "--password-stdin"],
58+
input=password,
59+
capture_output=True,
60+
text=True,
61+
timeout=timeout,
62+
)
63+
if result.returncode != 0:
64+
logger.error(f"Docker login to {registry} failed: {result.stderr.strip()}")
65+
raise subprocess.CalledProcessError(result.returncode, result.args, result.stdout, result.stderr)
66+
logger.info(f"Successfully logged in to {registry}")
67+
return result.stdout.strip()
68+
except subprocess.TimeoutExpired:
69+
logger.error(f"Docker login to {registry} timed out after {timeout}s")
70+
raise
71+
3972
@classmethod
4073
def remove_image(image: str) -> bytes:
4174
"""Remove a Docker image"""

0 commit comments

Comments
 (0)