feat(lab): bind sealed component adapter images

This commit is contained in:
DCCONSTRUCTIONS
2026-08-31 19:19:47 +03:00
parent 35d99e40d5
commit 19c0bd70d0
4 changed files with 210 additions and 64 deletions
@@ -0,0 +1,35 @@
FROM ndc/mission-core-lab-v1-goose:sg3.2.0-cu117-v1
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /opt/nodedc/adapter
ARG NODEDC_SHARED_ADAPTER_SHA256
ARG NODEDC_COMPONENT_ADAPTER_SHA256
COPY portable_lab_v1_component_adapter.py ./portable_lab_v1_component_adapter.py
COPY run_portable_lab_v1_ddrnet_component.py ./run_portable_lab_v1_ddrnet_component.py
RUN test "$(sha256sum portable_lab_v1_component_adapter.py | cut -d' ' -f1)" = \
"${NODEDC_SHARED_ADAPTER_SHA256}" \
&& test "$(sha256sum run_portable_lab_v1_ddrnet_component.py | cut -d' ' -f1)" = \
"${NODEDC_COMPONENT_ADAPTER_SHA256}" \
&& chmod 0444 portable_lab_v1_component_adapter.py \
run_portable_lab_v1_ddrnet_component.py \
&& conda run --no-capture-output --name goose python -B -m py_compile \
portable_lab_v1_component_adapter.py \
run_portable_lab_v1_ddrnet_component.py \
&& rm -rf __pycache__
LABEL org.opencontainers.image.title="NODE.DC LAB V1 DDRNet adapter" \
com.nodedc.product="mission-core" \
com.nodedc.stack="observatory" \
com.nodedc.component="lab-v1-ddrnet" \
com.nodedc.authority="observation-only" \
com.nodedc.base-image.sha256="591cb382c099eeb05e7ec16e2371e0b2da54d2bb5c49ec0f4ac88dbf72b0f0cd" \
com.nodedc.shared-adapter.sha256="${NODEDC_SHARED_ADAPTER_SHA256}" \
com.nodedc.component-adapter.sha256="${NODEDC_COMPONENT_ADAPTER_SHA256}"
ENTRYPOINT ["conda", "run", "--no-capture-output", "--name", "goose", "python"]
CMD ["/opt/nodedc/adapter/run_portable_lab_v1_ddrnet_component.py"]
@@ -0,0 +1,37 @@
FROM nvcr.io/nvidia/tritonserver:26.06-py3@sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794
ENV HF_HUB_OFFLINE=1 \
TRANSFORMERS_OFFLINE=1 \
PYTHONNOUSERSITE=1 \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /opt/nodedc/adapter
ARG NODEDC_SHARED_ADAPTER_SHA256
ARG NODEDC_COMPONENT_ADAPTER_SHA256
COPY portable_lab_v1_component_adapter.py ./portable_lab_v1_component_adapter.py
COPY run_portable_lab_v1_eomt_component.py ./run_portable_lab_v1_eomt_component.py
RUN test "$(sha256sum portable_lab_v1_component_adapter.py | cut -d' ' -f1)" = \
"${NODEDC_SHARED_ADAPTER_SHA256}" \
&& test "$(sha256sum run_portable_lab_v1_eomt_component.py | cut -d' ' -f1)" = \
"${NODEDC_COMPONENT_ADAPTER_SHA256}" \
&& chmod 0444 portable_lab_v1_component_adapter.py \
run_portable_lab_v1_eomt_component.py \
&& python3 -B -m py_compile portable_lab_v1_component_adapter.py \
run_portable_lab_v1_eomt_component.py \
&& rm -rf __pycache__
LABEL org.opencontainers.image.title="NODE.DC LAB V1 EoMT adapter" \
com.nodedc.product="mission-core" \
com.nodedc.stack="observatory" \
com.nodedc.component="lab-v1-eomt" \
com.nodedc.authority="observation-only" \
com.nodedc.base-image.sha256="58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794" \
com.nodedc.shared-adapter.sha256="${NODEDC_SHARED_ADAPTER_SHA256}" \
com.nodedc.component-adapter.sha256="${NODEDC_COMPONENT_ADAPTER_SHA256}"
ENTRYPOINT ["python3"]
CMD ["/opt/nodedc/adapter/run_portable_lab_v1_eomt_component.py"]
@@ -59,8 +59,6 @@ PORTABLE_LAB_V1_COMPONENT_INSTALLATION_SCHEMA: Final = (
_SETUP_ID: Final = "lab-v1-eomt-ddrnet-portable-v1"
_DEFINITION_ID: Final = "lab-v1-eomt-ddrnet-portable"
_DEFINITION_VERSION: Final = 2
_EOMT_IMAGE_SHA256: Final = "58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794"
_DDRNET_IMAGE_SHA256: Final = "591cb382c099eeb05e7ec16e2371e0b2da54d2bb5c49ec0f4ac88dbf72b0f0cd"
_EOMT_PIPELINE: Final = "recorded-semantic-eomt-fisheye-mask/v1"
_DDRNET_CHECKPOINT_SHA256: Final = (
"b99c2838051bcd7b092fd3970aa62a77d5c0bbb809c9b9afb2ff4b0ebdaa4ee6"
@@ -80,54 +78,56 @@ type PortableLabV1Component = Literal["eomt", "ddrnet"]
type PortableLabV1AssetKind = Literal["file", "tree"]
type PortableLabV1AssetVerification = Literal["sha256", "identity-sha256"]
_EXPECTED_ASSET_IDENTITIES: Final[dict[PortableLabV1Component, dict[str, str]]] = {
_EXPECTED_ASSET_TARGETS: Final[dict[PortableLabV1Component, dict[str, str]]] = {
"eomt": {
"eomt-config-json": ("7f4aa94fa4e43c0dbd79a5420edb511120aef62bd82bfbcbcece79948286a650"),
"eomt-dependency-set": ("4eb1f8d33236806e74f9e5bb96b7dce2ac37623dc39b2184be2aa8d7d00e983e"),
"eomt-evaluation-helper": (
"25baf30c0df564734e08f38ace88cc4bc147cacf240c761622279511e361daa4"
),
"eomt-model-weights": ("c265da9a74f58f5c3f4826d23ca4ca78beac0b106cca5842beca61580de5b782"),
"eomt-preprocessor-config": (
"97e2fbf7f0bdba2cfc90251c5133bae9c27ddc9c4410509f40670be2332854e7"
),
"eomt-profile": ("ea583966bc3409f5cf563cbf4fad05e366907e67187082eb692aff53d9f5d875"),
"eomt-profile-runtime": (
"01881862d4eaa218955f776a948124bf19c34be2b5ec282115daeacb15c53ae6"
),
"eomt-recorded-runtime": (
"4dcc4fc8bdf33702651a199be69d0dd4fadb243d2e65aee1c3d1ae7a58fdf675"
),
"eomt-runner": ("651e8e06c3912dffb036b7fd08f2c0623f7563d8306cc7aee05db562798518f4"),
"k1-valid-fov-identity": (
"b4dd8ddf2b87c1d520ee8a0868c4fea062d7c14d1bae73ccabd3abe1f3acbac2"
),
"k1-valid-fov-mask": ("a40cee06b7c6f69b6a09a11563dcfd237f3de833b1ccd31459e66692e528ba63"),
"eomt-environment": "/environment",
"eomt-ffmpeg-runtime": "/opt/ffmpeg",
"eomt-model-cache": "/cache",
"eomt-python-environment": "/opt/env",
"eomt-runner-bundle": "/runner",
"eomt-transformers-environment": "/opt/transformers",
"k1-valid-fov-root": "/valid-fov",
},
"ddrnet": {
"ddrnet-checkpoint": _DDRNET_CHECKPOINT_SHA256,
"ddrnet-goose-mapping": _GOOSE_MAPPING_SHA256,
"ddrnet-goose-runner": ("b18ad60f277eea69a240a28f290611b94627fb9707faf1bb3e6e22102dad67c1"),
"vegetation-policy": ("b75c4ac841d7b4bcc57f7a9c8417ca2317d8ecfa499e72a9af8a8591a2ec0d35"),
"vegetation-provider-map": (
"f2b69046b6a740fd9532d2d88e7fabae7c20fb662f783c9502adc9026406f352"
),
"ddrnet-checkpoint": "/opt/nodedc/assets/ddrnet-checkpoint",
"ddrnet-goose-mapping": "/opt/nodedc/assets/ddrnet-goose-mapping",
"ddrnet-goose-runner": "/opt/nodedc/assets/ddrnet-goose-runner",
"vegetation-policy": "/opt/nodedc/assets/vegetation-policy",
"vegetation-provider-map": "/opt/nodedc/assets/vegetation-provider-map",
},
}
_EXPECTED_FIXED_ASSET_IDENTITIES: Final[dict[str, str]] = {
"eomt-ffmpeg-runtime": "03651449fdcccec847a0f1241e1663a82cf374bd94e7470b4ddb0c0e46d88c69",
"ddrnet-checkpoint": _DDRNET_CHECKPOINT_SHA256,
"ddrnet-goose-mapping": _GOOSE_MAPPING_SHA256,
"ddrnet-goose-runner": "b18ad60f277eea69a240a28f290611b94627fb9707faf1bb3e6e22102dad67c1",
"vegetation-policy": "b75c4ac841d7b4bcc57f7a9c8417ca2317d8ecfa499e72a9af8a8591a2ec0d35",
"vegetation-provider-map": (
"f2b69046b6a740fd9532d2d88e7fabae7c20fb662f783c9502adc9026406f352"
),
}
_EXPECTED_ASSET_LENGTHS: Final[dict[str, int]] = {
"eomt-ffmpeg-runtime": 256_208_352,
"ddrnet-checkpoint": 259_419_077,
"ddrnet-goose-runner": 32_877,
"eomt-config-json": 1_575,
"eomt-evaluation-helper": 28_915,
"eomt-model-weights": 1_276_175_488,
"eomt-preprocessor-config": 666,
"eomt-profile": 3_805,
"eomt-profile-runtime": 45_789,
"eomt-recorded-runtime": 34_899,
"eomt-runner": 30_720,
"vegetation-policy": 3_022,
"vegetation-provider-map": 2_756,
}
_EXPECTED_ENTRYPOINTS: Final[dict[PortableLabV1Component, tuple[str, ...]]] = {
"eomt": ("python3",),
"ddrnet": (
"conda",
"run",
"--no-capture-output",
"--name",
"goose",
"python",
),
}
_EXPECTED_COMMANDS: Final[dict[PortableLabV1Component, tuple[str, ...]]] = {
"eomt": ("/opt/nodedc/adapter/run_portable_lab_v1_eomt_component.py",),
"ddrnet": ("/opt/nodedc/adapter/run_portable_lab_v1_ddrnet_component.py",),
}
class PortableLabV1LocalRunnerError(PortableLabV1WorkerError):
@@ -154,8 +154,18 @@ class PortableLabV1HostAsset:
_safe_token(self.asset_id, "asset id")
_host_path(self.host_path, "asset host path")
target = _container_path(self.container_path, "asset container path")
if target.parts[:4] != ("/", "opt", "nodedc", "assets"):
raise PortableLabV1LocalRunnerError("asset target is outside /opt/nodedc/assets")
expected_target = next(
(
paths[self.asset_id]
for paths in _EXPECTED_ASSET_TARGETS.values()
if self.asset_id in paths
),
None,
)
if expected_target is None or target.as_posix() != expected_target:
raise PortableLabV1LocalRunnerError(
"asset target differs from its fixed component path"
)
_digest(self.identity_sha256, "asset identity sha256")
if self.kind == "file" and self.verification != "sha256":
raise PortableLabV1LocalRunnerError(
@@ -210,11 +220,15 @@ class PortableLabV1ComponentInstallation:
if self.component not in ("eomt", "ddrnet"):
raise PortableLabV1LocalRunnerError("component identity is invalid")
_digest(self.image_sha256, "component image sha256")
expected_image = _EOMT_IMAGE_SHA256 if self.component == "eomt" else _DDRNET_IMAGE_SHA256
if self.image_sha256 != expected_image:
raise PortableLabV1LocalRunnerError(f"{self.component} image identity changed")
_argv(self.entrypoint, "component entrypoint")
_argv(self.command, "component command")
if (
self.entrypoint != _EXPECTED_ENTRYPOINTS[self.component]
or self.command != _EXPECTED_COMMANDS[self.component]
):
raise PortableLabV1LocalRunnerError(
f"{self.component} adapter entrypoint changed"
)
if not (1.0 <= self.timeout_seconds <= 24 * 60 * 60):
raise PortableLabV1LocalRunnerError("component timeout is invalid")
if not (1024**3 <= self.memory_bytes <= 128 * 1024**3):
@@ -227,10 +241,31 @@ class PortableLabV1ComponentInstallation:
targets = tuple(asset.container_path for asset in self.assets)
if len(targets) != len(set(targets)):
raise PortableLabV1LocalRunnerError("component asset targets overlap")
expected = _EXPECTED_ASSET_IDENTITIES[self.component]
expected = _EXPECTED_ASSET_TARGETS[self.component]
if set(asset_ids) != set(expected):
raise PortableLabV1LocalRunnerError(f"{self.component} installed asset set changed")
if any(asset.identity_sha256 != expected[asset.asset_id] for asset in self.assets):
if any(asset.container_path != expected[asset.asset_id] for asset in self.assets):
raise PortableLabV1LocalRunnerError(
f"{self.component} installed asset target changed"
)
expected_kind: PortableLabV1AssetKind = (
"tree" if self.component == "eomt" else "file"
)
expected_verification: PortableLabV1AssetVerification = (
"identity-sha256" if self.component == "eomt" else "sha256"
)
if any(
asset.kind != expected_kind or asset.verification != expected_verification
for asset in self.assets
):
raise PortableLabV1LocalRunnerError(
f"{self.component} installed asset verification changed"
)
if any(
asset.asset_id in _EXPECTED_FIXED_ASSET_IDENTITIES
and asset.identity_sha256 != _EXPECTED_FIXED_ASSET_IDENTITIES[asset.asset_id]
for asset in self.assets
):
raise PortableLabV1LocalRunnerError(
f"{self.component} installed asset identity changed"
)
@@ -440,11 +475,14 @@ class PortableLabV1DockerLaunch:
if self.component not in ("eomt", "ddrnet"):
raise PortableLabV1LocalRunnerError("launch component is invalid")
_digest(self.image_sha256, "launch image sha256")
expected_image = _EOMT_IMAGE_SHA256 if self.component == "eomt" else _DDRNET_IMAGE_SHA256
if self.image_sha256 != expected_image:
raise PortableLabV1LocalRunnerError("launch image identity changed")
_argv(self.entrypoint, "launch entrypoint")
_argv(self.command, "launch command")
if (
self.entrypoint != _EXPECTED_ENTRYPOINTS[self.component]
or self.command[:-2] != _EXPECTED_COMMANDS[self.component]
or self.command[-2:] != ("--request", "/run/nodedc/request.json")
):
raise PortableLabV1LocalRunnerError("launch adapter command changed")
_safe_token(self.name_token, "launch name token")
if not (1.0 <= self.timeout_seconds <= 24 * 60 * 60):
raise PortableLabV1LocalRunnerError("launch timeout is invalid")
@@ -458,10 +496,11 @@ class PortableLabV1DockerLaunch:
writable = tuple(mount for mount in self.mounts if not mount.read_only)
if len(writable) != 1 or writable[0].container_path != "/run/nodedc/output":
raise PortableLabV1LocalRunnerError("Docker writable mount surface changed")
allowed_asset_targets = set(_EXPECTED_ASSET_TARGETS[self.component].values())
if any(
not (
mount.container_path.startswith("/run/nodedc/")
or mount.container_path.startswith("/opt/nodedc/assets/")
or mount.container_path in allowed_asset_targets
)
for mount in self.mounts
):
@@ -1,5 +1,6 @@
from __future__ import annotations
import hashlib
import json
from dataclasses import dataclass
from pathlib import Path
@@ -21,6 +22,7 @@ EOMT_IMAGE_SHA256 = "58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf
DDRNET_IMAGE_SHA256 = "591cb382c099eeb05e7ec16e2371e0b2da54d2bb5c49ec0f4ac88dbf72b0f0cd"
DEFINITION_SHA256 = "a" * 64
RELEASE_SHA256 = "b" * 64
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
@dataclass(frozen=True)
@@ -42,19 +44,21 @@ class _Plan:
def _assets(
component: local_runners.PortableLabV1Component,
) -> tuple[local_runners.PortableLabV1HostAsset, ...]:
identities = local_runners._EXPECTED_ASSET_IDENTITIES[component]
tree_assets = {"eomt-dependency-set", "k1-valid-fov-identity"}
targets = local_runners._EXPECTED_ASSET_TARGETS[component]
return tuple(
local_runners.PortableLabV1HostAsset(
asset_id=asset_id,
host_path=f"D:\\NDC_MISSIONCORE\\assets\\{asset_id}",
container_path=f"/opt/nodedc/assets/{asset_id}",
kind="tree" if asset_id in tree_assets else "file",
verification=("identity-sha256" if asset_id in tree_assets else "sha256"),
identity_sha256=identity,
byte_length=local_runners._EXPECTED_ASSET_LENGTHS.get(asset_id),
container_path=container_path,
kind="tree" if component == "eomt" else "file",
verification=("identity-sha256" if component == "eomt" else "sha256"),
identity_sha256=local_runners._EXPECTED_FIXED_ASSET_IDENTITIES.get(
asset_id,
hashlib.sha256(asset_id.encode()).hexdigest(),
),
byte_length=local_runners._EXPECTED_ASSET_LENGTHS.get(asset_id, 1_024),
)
for asset_id, identity in sorted(identities.items())
for asset_id, container_path in sorted(targets.items())
)
@@ -64,8 +68,8 @@ def _component(
return local_runners.PortableLabV1ComponentInstallation.seal(
component=component,
image_sha256=(EOMT_IMAGE_SHA256 if component == "eomt" else DDRNET_IMAGE_SHA256),
entrypoint=("/opt/nodedc/bin/portable-lab-v1-agent",),
command=(f"run-{component}",),
entrypoint=local_runners._EXPECTED_ENTRYPOINTS[component],
command=local_runners._EXPECTED_COMMANDS[component],
assets=_assets(component),
timeout_seconds=3600.0,
memory_bytes=16 * 1024**3,
@@ -148,8 +152,12 @@ def _docker_launch(tmp_path: Path) -> local_runners.PortableLabV1DockerLaunch:
return local_runners.PortableLabV1DockerLaunch(
component="eomt",
image_sha256=EOMT_IMAGE_SHA256,
entrypoint=("/opt/nodedc/bin/portable-lab-v1-agent",),
command=("run-eomt", "--request", "/run/nodedc/request.json"),
entrypoint=local_runners._EXPECTED_ENTRYPOINTS["eomt"],
command=(
"/opt/nodedc/adapter/run_portable_lab_v1_eomt_component.py",
"--request",
"/run/nodedc/request.json",
),
mounts=(
local_runners.PortableLabV1DockerMount(
host_path="D:\\NDC_MISSIONCORE\\runtime\\output",
@@ -522,10 +530,37 @@ def test_component_installation_rejects_wrong_worker006_asset_identity() -> None
local_runners.PortableLabV1ComponentInstallation.seal(
component="ddrnet",
image_sha256=DDRNET_IMAGE_SHA256,
entrypoint=("/opt/nodedc/bin/portable-lab-v1-agent",),
command=("run-ddrnet",),
entrypoint=local_runners._EXPECTED_ENTRYPOINTS["ddrnet"],
command=local_runners._EXPECTED_COMMANDS["ddrnet"],
assets=tuple(assets),
timeout_seconds=3600.0,
memory_bytes=16 * 1024**3,
nano_cpus=4_000_000_000,
)
def test_component_adapter_images_are_thin_offline_wrappers() -> None:
eomt = (
REPOSITORY_ROOT
/ "experiments/perception/worker/observatory_portable/"
"Dockerfile.lab-v1-eomt-adapter"
).read_text(encoding="utf-8")
ddrnet = (
REPOSITORY_ROOT
/ "experiments/perception/worker/observatory_portable/"
"Dockerfile.lab-v1-ddrnet-adapter"
).read_text(encoding="utf-8")
assert "FROM nvcr.io/nvidia/tritonserver:26.06-py3@sha256:" in eomt
assert "FROM ndc/mission-core-lab-v1-goose:sg3.2.0-cu117-v1" in ddrnet
assert "COPY portable_lab_v1_component_adapter.py" in eomt
assert "COPY portable_lab_v1_component_adapter.py" in ddrnet
assert 'ENTRYPOINT ["python3"]' in eomt
assert 'ENTRYPOINT ["conda", "run", "--no-capture-output"' in ddrnet
for dockerfile in (eomt, ddrnet):
assert "apt-get" not in dockerfile
assert "pip install" not in dockerfile
assert "curl " not in dockerfile
assert "wget " not in dockerfile
assert "NODEDC_SHARED_ADAPTER_SHA256" in dockerfile
assert "NODEDC_COMPONENT_ADAPTER_SHA256" in dockerfile