mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
docker info reports DockerRootDir as the DAEMON sees it, and
host_storage_constrained fed that string straight into shutil.disk_usage,
which statvfs()es it in the CALLING process's mount namespace. The two
coincide only when the caller is the daemon's host. In the nested act
container of the workspace jobs they do not, so os.statvfs('/var/lib/docker')
raised FileNotFoundError, matrix.py died, and because 00_one.sh invokes it
bare under set -euo pipefail the whole swarm drill went down with it. All
five workspace jobs of run 30560683608 failed this way, byte-identically,
across five base images and five nested distros.
docker_root_free_bytes() now takes the vantage point as a required keyword:
it measures the daemon's data root when that path resolves here, and the
caller's stated local path when it does not. matrix.py passes "/", which is
the same vantage it already trusts for its ENOSPC abort 270 lines earlier;
overlayfs passes statfs through to the upper filesystem, so a container of a
daemon measures that daemon's data-root filesystem through its own rootfs.
Nothing is caught and nothing is guessed. An unreachable daemon still raises
RuntimeError out of docker_data_root(), which is the guard this module exists
for, and local_vantage carries no default because only the caller knows where
it is running.
The compose-side twin loses its || true and its hardcoded /var/lib/docker
substitution, so an unresolvable root no longer measures a guessed filesystem.
Its compose.exec locality was already correct and is unchanged.
Verified against the pre-fix module under the CI condition: with the data root
stubbed to a path absent from this namespace, the old code raises
FileNotFoundError and the new code measures the local vantage; with the daemon
stubbed unreachable, the new code still raises RuntimeError.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
45 lines
1.2 KiB
Python
45 lines
1.2 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from utils.storage.constrained import is_constrained, required_storage_bytes
|
|
|
|
if TYPE_CHECKING:
|
|
from .compose import Compose
|
|
|
|
|
|
def detect_storage_constrained(
|
|
compose: Compose, app_ids: list[str], variants: dict[str, int] | None = None
|
|
) -> bool:
|
|
"""
|
|
Return True if the declared storage need of app_ids and their transitive
|
|
dependencies, at the given variant overlay, exceeds the free space on the
|
|
filesystem holding DockerRootDir.
|
|
|
|
We intentionally measure the DockerRootDir filesystem because this is where
|
|
images/volumes/build cache usually grow (especially in CI / Docker-in-Docker).
|
|
"""
|
|
cmd = [
|
|
"bash",
|
|
"-lc",
|
|
r"""
|
|
set -euo pipefail
|
|
root="$(docker info -f '{{.DockerRootDir}}')"
|
|
df -PB1 "${root}" | awk 'NR==2{print $4}'
|
|
""",
|
|
]
|
|
|
|
r = compose.exec(cmd, check=False, capture=True)
|
|
if r.returncode != 0:
|
|
return False
|
|
|
|
try:
|
|
free_bytes = int((r.stdout or "").strip())
|
|
except ValueError:
|
|
return False
|
|
|
|
return is_constrained(
|
|
free_bytes=free_bytes,
|
|
required_bytes=required_storage_bytes(app_ids, variants),
|
|
)
|