diff --git a/experiments/perception/worker/observatory_portable/Dockerfile.lab-v1-ddrnet-adapter b/experiments/perception/worker/observatory_portable/Dockerfile.lab-v1-ddrnet-adapter new file mode 100644 index 0000000..0659689 --- /dev/null +++ b/experiments/perception/worker/observatory_portable/Dockerfile.lab-v1-ddrnet-adapter @@ -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"] diff --git a/experiments/perception/worker/observatory_portable/Dockerfile.lab-v1-eomt-adapter b/experiments/perception/worker/observatory_portable/Dockerfile.lab-v1-eomt-adapter new file mode 100644 index 0000000..2a5c105 --- /dev/null +++ b/experiments/perception/worker/observatory_portable/Dockerfile.lab-v1-eomt-adapter @@ -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"] diff --git a/src/k1link/observatory/portable_lab_v1_local_runners.py b/src/k1link/observatory/portable_lab_v1_local_runners.py index 187711d..196fe4c 100644 --- a/src/k1link/observatory/portable_lab_v1_local_runners.py +++ b/src/k1link/observatory/portable_lab_v1_local_runners.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 ): diff --git a/tests/test_observatory_portable_lab_v1_local_runners.py b/tests/test_observatory_portable_lab_v1_local_runners.py index e0ae168..ab0852a 100644 --- a/tests/test_observatory_portable_lab_v1_local_runners.py +++ b/tests/test_observatory_portable_lab_v1_local_runners.py @@ -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