Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ admin = [
"websockets>=15.0.1",
"aiohttp>=3.12.15",
"alibabacloud_cr20181201==2.0.5",
"aliyun-python-sdk-cr",
"sqlmodel",
"aiosqlite",
"asyncpg",
Expand Down
11 changes: 11 additions & 0 deletions rock-conf/rock-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ warmup:
# - "reg-a.aliyuncs.com/mirror-1"
# - "reg-b.aliyuncs.com/mirror-2"

# ACR registry and builder configuration
acr:
registry:
instance_id: ""
namespace: "rock"
registry_url: ""
region: "cn-hangzhou"
access_key_id: ""
access_key_secret: ""
builder_image: ""

# Scheduler configuration
scheduler:
enabled: true # Whether to enable the scheduler
Expand Down
8 changes: 8 additions & 0 deletions rock/admin/entrypoints/sandbox_proxy_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,14 @@ async def get_token(account: str = "legacy"):
return RockResponse(result=result)


@sandbox_proxy_router.get("/acr_config")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

改成restful风格的api

@handle_exceptions(error_message="get acr config failed")
async def get_acr_config():
"""Return ACR registry config with temporary credentials."""
result = await asyncio.to_thread(sandbox_proxy_service.get_acr_config)
return RockResponse(result=result)


@sandbox_proxy_router.api_route(
"/sandboxes/{sandbox_id}/vnc",
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"],
Expand Down
25 changes: 25 additions & 0 deletions rock/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,28 @@ def __post_init__(self):
self.primary = OssAccountConfig(**self.primary)


@dataclass
class AcrRegistryConfig:
instance_id: str | None = None
namespace: str = "rock"
registry_url: str | None = None
region: str | None = None

# Long-lived AK/SK for the ACR AcsClient (admin-side only, never exposed to SDK).
access_key_id: str = ""
access_key_secret: str = ""


@dataclass
class AcrConfig:
registry: AcrRegistryConfig = field(default_factory=AcrRegistryConfig)
builder_image: str = ""

def __post_init__(self):
if isinstance(self.registry, dict):
self.registry = AcrRegistryConfig(**self.registry)


@dataclass
class ProxyServiceConfig:
timeout: float = 180.0
Expand Down Expand Up @@ -348,6 +370,7 @@ class RockConfig:
redis: RedisConfig = field(default_factory=RedisConfig)
sandbox_config: SandboxConfig = field(default_factory=SandboxConfig)
oss: OssConfig = field(default_factory=OssConfig)
acr: AcrConfig = field(default_factory=AcrConfig)
runtime: RuntimeConfig = field(default_factory=RuntimeConfig)
proxy_service: ProxyServiceConfig = field(default_factory=ProxyServiceConfig)
scheduler: SchedulerConfig = field(default_factory=SchedulerConfig)
Expand Down Expand Up @@ -401,6 +424,8 @@ def from_env(cls, config_path: str | None = None):
kwargs["sandbox_config"] = SandboxConfig(**config["sandbox_config"])
if "oss" in config:
kwargs["oss"] = OssConfig(**config["oss"])
if "acr" in config:
kwargs["acr"] = AcrConfig(**config["acr"])
if "runtime" in config:
kwargs["runtime"] = RuntimeConfig(**config["runtime"])
if "proxy_service" in config:
Expand Down
45 changes: 44 additions & 1 deletion rock/sandbox/service/sandbox_proxy_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import websockets
from aliyunsdkcore import client
from aliyunsdkcore.request import CommonRequest

from aliyunsdkcr.request.v20181201 import GetAuthorizationTokenRequest
from fastapi import Response, UploadFile
from starlette.status import HTTP_504_GATEWAY_TIMEOUT

Expand All @@ -34,7 +36,7 @@
from rock.admin.proto.request import SandboxReadFileRequest as ReadFileRequest
from rock.admin.proto.request import SandboxWriteFileRequest as WriteFileRequest
from rock.admin.proto.response import SandboxListResponse, SandboxListStatusResponse, SandboxStatusResponse
from rock.config import OssConfig, ProxyServiceConfig, RockConfig
from rock.config import AcrConfig, OssConfig, ProxyServiceConfig, RockConfig
from rock.deployments.constants import Port
from rock.deployments.status import ServiceStatus
from rock.common.port_validation import validate_port_forward_port
Expand Down Expand Up @@ -91,6 +93,16 @@ def __init__(self, rock_config: RockConfig, meta_store: SandboxMetaStore):
primary_region,
)

self.acr_config: AcrConfig = rock_config.acr
self._acr_client = None
if self.acr_config.registry.access_key_id and self.acr_config.registry.instance_id:
acr_region = self.acr_config.registry.region or "cn-hangzhou"
self._acr_client = client.AcsClient(
self.acr_config.registry.access_key_id,
self.acr_config.registry.access_key_secret,
acr_region,
)

self._batch_get_status_max_count = rock_config.proxy_service.batch_get_status_max_count
self._validate_oss_config_or_warn()

Expand Down Expand Up @@ -746,6 +758,37 @@ def gen_oss_sts_token(
"Prefix": prefix, # transfer-object key prefix, scoped per account
}

def get_acr_config(self) -> dict | None:
"""Return ACR registry config with temporary credentials.

Uses the ACR ``GetAuthorizationToken`` API to obtain a short-lived
username/password pair (1 hour) for image push/pull operations.
Returns ``None`` when ACR is not configured.
"""
if self._acr_client is None:
logger.warning("ACR client not configured (missing access_key_id or instance_id)")
return None

registry = self.acr_config.registry

request = GetAuthorizationTokenRequest.GetAuthorizationTokenRequest()
request.set_InstanceId(registry.instance_id)
try:
body = self._acr_client.do_action_with_exception(request)
data = json.loads(body)
except Exception:
logger.error("generate ACR authorization token failed", exc_info=True)
return None

return {
"Registry": registry.registry_url,
"Namespace": registry.namespace,
"Username": data.get("TempUsername"),
"Password": data.get("AuthorizationToken"),
"Expiration": data.get("ExpireTime"),
"BuilderImage": self.acr_config.builder_image,
}

async def get_sandbox_websocket_url(
self, sandbox_id: str, target_path: str | None = None, port: int | None = None
) -> str:
Expand Down
62 changes: 61 additions & 1 deletion tests/unit/sandbox/test_sandbox_proxy.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import json
import uuid
from unittest.mock import MagicMock, patch

import pytest

from rock.actions.sandbox.response import State
from rock.config import OssConfig
from rock.config import AcrConfig, AcrRegistryConfig, OssConfig
from rock.deployments.config import DockerDeploymentConfig
from rock.sandbox.sandbox_manager import SandboxManager
from rock.sandbox.service.sandbox_proxy_service import SandboxProxyService
Expand Down Expand Up @@ -208,3 +209,62 @@ def test_yaml_used_when_env_var_empty(self, sandbox_proxy_service):
assert result["Endpoint"] == "yaml.endpoint" # YAML fallback
assert result["Bucket"] == "yaml-bucket"
assert result["Region"] == "rg" # env


class TestGetAcrConfig:
@pytest.fixture
def proxy_service(self):
service = SandboxProxyService.__new__(SandboxProxyService)
service.acr_config = AcrConfig(
registry=AcrRegistryConfig(
instance_id="cri-test123",
namespace="my-ns",
registry_url="reg.example.com",
region="cn-hangzhou",
access_key_id="ak",
access_key_secret="sk",
),
builder_image="builder:latest",
)
service._acr_client = MagicMock()
return service

@pytest.fixture(autouse=True)
def _mock_acr_sdk(self):
mock_module = MagicMock()
with patch.dict("sys.modules", {"aliyunsdkcr": mock_module, "aliyunsdkcr.request": mock_module, "aliyunsdkcr.request.v20181201": mock_module, "aliyunsdkcr.request.v20181201.GetAuthorizationTokenRequest": mock_module}):
yield

def test_success_returns_config_and_credentials(self, proxy_service):
fake_response = json.dumps(
{
"TempUsername": "tmp-user",
"AuthorizationToken": "tmp-pass-token",
"ExpireTime": "2099-01-01T00:15:00Z",
}
).encode()
proxy_service._acr_client.do_action_with_exception.return_value = fake_response

result = proxy_service.get_acr_config()

assert result is not None
assert result["Registry"] == "reg.example.com"
assert result["Namespace"] == "my-ns"
assert result["Username"] == "tmp-user"
assert result["Password"] == "tmp-pass-token"
assert result["Expiration"] == "2099-01-01T00:15:00Z"
assert result["BuilderImage"] == "builder:latest"

def test_acr_failure_returns_none(self, proxy_service):
proxy_service._acr_client.do_action_with_exception.side_effect = Exception("acr fail")

result = proxy_service.get_acr_config()

assert result is None

def test_no_acr_client_returns_none(self, proxy_service):
proxy_service._acr_client = None

result = proxy_service.get_acr_config()

assert result is None
4 changes: 1 addition & 3 deletions tests/unit/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,7 @@ def test_sandbox_log_config_defaults():
from rock.config import SandboxLogConfig

cfg = SandboxLogConfig()
# prefix defaults empty: each deployment YAML must opt-in to a value
# matching its OSS bucket lifecycle rule (e.g. "rock-archives/").
assert cfg.archive_prefix == ""
assert cfg.archive_prefix == "rock-archives/"
assert cfg.keep_days_before_archive == 3
assert cfg.archive_max_attempts == 3

Expand Down
15 changes: 15 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading