Publish curated ITWorx-ModelForge source from 990401737f96c8d46a9f7b0d84994516cadcb6a0
This commit is contained in:
1 parent
7082ab955a
commit
848f0d8f8a
434 files changed
+67298
-4315
No files matched your search
+56
-21
@@ -1,27 +1,59 @@
|
||||
FROM python:3.12-slim-trixie@sha256:09f7da3bc104798d0afb40bc08d23ab2da20a76130cec1f2ef170848f5d85217 AS builder
|
||||
# Canonical's maintained glibc-compatible Python runtime is kept as a named stage so the builder
|
||||
# can preserve its passwd/group contract without adding a shell or package manager to the final
|
||||
# image.
|
||||
FROM ubuntu/python:3.12-24.04@sha256:d5b4c1e1da458c6e10da9d7f42276d8d640ed5a0d87b1cced734a9434c531a62 AS runtime-base
|
||||
|
||||
# The builder is deliberately separate from the runtime. The agent only needs the Python
|
||||
# application packages at runtime; a full Debian build environment must not become part of the
|
||||
# shipped image or its vulnerability surface.
|
||||
FROM python:3.12-slim-trixie@sha256:e5c9fa26ffb76e11e0f054f30dc2523a2f9693f0c36c0cf1e39b27e152d899fc AS builder
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
WORKDIR /build
|
||||
COPY config/security/build-toolchain.lock /tmp/modelforge-build-toolchain.lock
|
||||
COPY backend ./backend
|
||||
COPY node-agent ./node-agent
|
||||
RUN python -m venv /opt/venv \
|
||||
&& /opt/venv/bin/pip install --no-cache-dir --upgrade pip "setuptools>=78.1.1" "msgpack>=1.2.1" \
|
||||
&& /opt/venv/bin/pip install --no-cache-dir ./backend ./node-agent \
|
||||
&& /opt/venv/bin/pip check \
|
||||
&& /opt/venv/bin/python -m pip uninstall --yes pip setuptools
|
||||
|
||||
FROM python:3.12-slim-trixie@sha256:09f7da3bc104798d0afb40bc08d23ab2da20a76130cec1f2ef170848f5d85217
|
||||
# Install into a plain target directory so the final image can use Canonical's maintained,
|
||||
# glibc-compatible chiselled Python runtime. Hash-locked build tooling is removed inside the
|
||||
# builder only; no installer or build-backend metadata is carried into the runtime image. The
|
||||
# target install ignores the builder venv so a build-only package cannot mask a runtime dependency.
|
||||
# The import check exercises the same dependency closure that the final image receives without
|
||||
# contacting the control plane or touching a model repository.
|
||||
RUN python -m venv /opt/build-venv \
|
||||
&& /opt/build-venv/bin/python -m pip install --no-cache-dir --disable-pip-version-check \
|
||||
--require-hashes --only-binary=:all: --index-url https://pypi.org/simple \
|
||||
-r /tmp/modelforge-build-toolchain.lock \
|
||||
&& /opt/build-venv/bin/python -m pip install --no-cache-dir --disable-pip-version-check \
|
||||
--no-build-isolation --ignore-installed --no-compile \
|
||||
--target /opt/site-packages ./backend ./node-agent \
|
||||
&& PYTHONPATH=/opt/site-packages /opt/build-venv/bin/python -m pip check \
|
||||
&& PYTHONPATH=/opt/site-packages /opt/build-venv/bin/python -c \
|
||||
"import httpx, psutil, pynvml, modelforge_api, modelforge_node_agent" \
|
||||
&& /opt/build-venv/bin/python -m pip uninstall --yes pip setuptools wheel editables hatchling pathspec \
|
||||
pluggy tomlkit trove-classifiers
|
||||
|
||||
# Prepare the fixed runtime contract without requiring a shell or package manager in the final
|
||||
# image. The numeric IDs are intentional: Compose and existing volumes rely on UID 100/GID 101.
|
||||
RUN mkdir -p /runtime/app /runtime/data/state /runtime/data/hf-cache \
|
||||
/runtime/data/artifacts /runtime/data/quarantine \
|
||||
&& chown -R 100:101 /runtime/app /runtime/data
|
||||
|
||||
# Preserve the canonical runtime's passwd/group entries and add the existing ModelForge identity
|
||||
# with its established home directory. The final image remains shell-less and runs numerically as
|
||||
# UID 100/GID 101, while libraries that resolve the process user still see the same contract.
|
||||
COPY --from=runtime-base /etc/passwd /runtime/etc/passwd
|
||||
COPY --from=runtime-base /etc/group /runtime/etc/group
|
||||
RUN python -c "from pathlib import Path; p=Path('/runtime/etc/passwd'); s=p.read_text(); p.write_text(s if '\\nmodelforge-agent:' in s else s.rstrip('\\n')+'\\nmodelforge-agent:x:100:101::/app:/usr/sbin/nologin\\n'); p=Path('/runtime/etc/group'); s=p.read_text(); p.write_text(s if '\\nmodelforge-agent:' in s else s.rstrip('\\n')+'\\nmodelforge-agent:x:101:\\n')"
|
||||
|
||||
FROM runtime-base
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PATH="/opt/venv/bin:${PATH}"
|
||||
|
||||
RUN apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get upgrade --yes \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& /usr/local/bin/python -m pip uninstall --yes pip setuptools
|
||||
PYTHONPATH=/opt/site-packages \
|
||||
HOME=/app
|
||||
|
||||
# Build identity. These are stamped in at build time so a running container can say exactly
|
||||
# where it came from; an argument that is never passed stays empty and is reported as null
|
||||
@@ -36,17 +68,20 @@ LABEL org.opencontainers.image.description="Outbound-only compute node agent for
|
||||
LABEL org.opencontainers.image.version="${MODELFORGE_VERSION}"
|
||||
LABEL org.opencontainers.image.revision="${MODELFORGE_COMMIT}"
|
||||
LABEL org.opencontainers.image.created="${MODELFORGE_BUILT_AT}"
|
||||
LABEL org.opencontainers.image.source="https://git.example.com/example/modelforge.git"
|
||||
LABEL org.opencontainers.image.source="https://git.example.com/example/modelforge"
|
||||
LABEL org.opencontainers.image.vendor="ITWorx"
|
||||
LABEL org.opencontainers.image.licenses="AGPL-3.0-or-later"
|
||||
|
||||
COPY --from=builder /opt/venv /opt/venv
|
||||
COPY --from=builder --chown=100:101 /opt/site-packages /opt/site-packages
|
||||
COPY --from=builder --chown=100:101 /runtime/app /app
|
||||
COPY --from=builder --chown=100:101 /runtime/data /data
|
||||
COPY --from=builder /runtime/etc/passwd /etc/passwd
|
||||
COPY --from=builder /runtime/etc/group /etc/group
|
||||
|
||||
RUN groupadd --system --gid 101 modelforge-agent \
|
||||
&& useradd --system --uid 100 --gid 101 --home-dir /app --create-home modelforge-agent \
|
||||
&& mkdir -p /data/state /data/hf-cache /data/artifacts /data/quarantine \
|
||||
&& chown -R modelforge-agent:modelforge-agent /data
|
||||
WORKDIR /app
|
||||
USER modelforge-agent
|
||||
USER 100:101
|
||||
|
||||
CMD ["modelforge-node-agent"]
|
||||
# Canonical's rock image has a Pebble entrypoint. The agent has no init supervisor requirement,
|
||||
# so explicitly remove it and invoke the reviewed Python module directly.
|
||||
ENTRYPOINT []
|
||||
CMD ["python3", "-m", "modelforge_node_agent.main"]
|
||||
@@ -1,10 +1,12 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=80"]
|
||||
requires = [
|
||||
"setuptools==84.0.0",
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "modelforge-node-agent"
|
||||
version = "1.2.2"
|
||||
version = "1.3.0-rc.1"
|
||||
license = "AGPL-3.0-or-later"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
@@ -13,13 +15,13 @@ dependencies = [
|
||||
# Ships as one product with the control plane, so this tracks VERSION exactly. A
|
||||
# stale pin here made the Node Agent image unbuildable the moment the product
|
||||
# version moved, which is a release failure that only appears at build time.
|
||||
"modelforge-api==1.2.2",
|
||||
"modelforge-api==1.3.0-rc.1",
|
||||
"pydantic-settings==2.15.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest==8.4.2",
|
||||
"pytest==9.0.3",
|
||||
"ruff==0.16.5",
|
||||
"mypy==1.20.2",
|
||||
]
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.2.2"
|
||||
__version__ = "1.3.0-rc.1"
|
||||
@@ -5,6 +5,8 @@ import os
|
||||
from dataclasses import asdict, dataclass
|
||||
from pathlib import Path
|
||||
|
||||
CREDENTIAL_SHARED_READ_MODE = 0o640
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentPersistentState:
|
||||
@@ -36,11 +38,17 @@ class AgentStateStore:
|
||||
def credential(self) -> str | None:
|
||||
try:
|
||||
value = self.credential_file.read_text(encoding="utf-8").strip()
|
||||
return value or None
|
||||
if not value:
|
||||
return None
|
||||
# The runtime worker has a separate non-root identity. It joins the dedicated agent
|
||||
# group solely to read this credential from the shared, read-only state volume.
|
||||
# Normalising an older 0600 credential here makes ordinary Agent upgrades safe too.
|
||||
os.chmod(self.credential_file, CREDENTIAL_SHARED_READ_MODE)
|
||||
return value
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
def save_credential(self, credential: str) -> None:
|
||||
self.credential_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.credential_file.write_text(credential, encoding="utf-8")
|
||||
os.chmod(self.credential_file, 0o600)
|
||||
os.chmod(self.credential_file, CREDENTIAL_SHARED_READ_MODE)
|
||||
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from unittest.mock import call, patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
@@ -182,6 +183,24 @@ def test_first_enrollment_and_restart_reuse_persisted_credential(tmp_path: Path)
|
||||
assert restarted.credential == first.credential
|
||||
|
||||
|
||||
def test_persisted_credential_is_group_readable_for_the_runtime_worker(tmp_path: Path) -> None:
|
||||
with patch("modelforge_node_agent.state.os.chmod") as chmod:
|
||||
agent = build_agent(tmp_path, FakeTransport())
|
||||
agent.ensure_enrolled()
|
||||
credential_file = agent.settings.credential_file
|
||||
assert chmod.call_args_list == [call(credential_file, 0o640)]
|
||||
|
||||
# An upgrade can retain the stricter historical mode. The Agent owns the file and repairs
|
||||
# only that mode at startup so the separately non-root runtime worker can read, never
|
||||
# write, its node credential.
|
||||
restarted = build_agent(tmp_path, FakeTransport(), token=None)
|
||||
assert restarted.credential == agent.credential
|
||||
assert chmod.call_args_list == [
|
||||
call(credential_file, 0o640),
|
||||
call(credential_file, 0o640),
|
||||
]
|
||||
|
||||
|
||||
def test_agent_advertises_typed_runtime_worker_capabilities(tmp_path: Path) -> None:
|
||||
capabilities = build_agent(tmp_path, FakeTransport()).metadata().supported_capabilities
|
||||
assert "runtime.probe.v1" in capabilities
|
||||
|
||||
Reference in new issue
Block a user