package system // Docker-data volume headroom — the infra-protection prevention layer (storage-split slice). // // After the OS/Docker-data split, /var/lib/docker is a dedicated volume holding ALL images + // overlay + named volumes (controller/traefik/cloudflared/filebrowser AND customer apps). Infra is // protected by PREVENTION, not placement: a reserved buffer the controller refuses to deploy into, // so the volume can't be filled to the point the infra containers can't write. This file measures // that volume and computes the reserved-buffer verdict; the deploy gate + UI consume it. // DockerVolumePath is the path whose filesystem backs Docker's data-root as seen from INSIDE the // controller container. The controller's own root ("/") is an overlay whose upperdir lives on the // guest's /var/lib/docker volume, so statfs("/") reports THAT volume's capacity/free — i.e. the // Docker-data volume the split isolates (and, pre-split, the rootfs — correct either way: it is // always wherever Docker's data-root lives). const DockerVolumePath = "/" // DockerVolumeReserveGB returns the reserved-buffer floor (GiB) for the Docker-data volume: // max(5 GB, 10% of total). Deploys are refused once free space reaches this floor so the infra // containers keep running even when apps would otherwise fill the volume. (10% rather than the // Tier-2 guard's 20%: on a large data volume 20% would reserve an absurd amount; infra needs only // modest headroom for logs/overlay writes, and the runtime disk-warning at 80% used trips first.) func DockerVolumeReserveGB(totalGB float64) float64 { reserve := totalGB * 0.10 if reserve < 5.0 { reserve = 5.0 } return reserve } // DockerVolumeHeadroom is the Docker-data volume's capacity view for the prevention layer. type DockerVolumeHeadroom struct { TotalGB float64 AvailGB float64 ReserveGB float64 BelowReserve bool // free space is at/under the reserved buffer → refuse new deploys OK bool // stats were readable (false → callers must FAIL-OPEN, not block) } // GetDockerVolumeHeadroom measures the Docker-data volume and computes the reserved-buffer verdict. // OK=false when the stats can't be read; callers MUST fail-open (do not block deploys on a transient // measurement error — the buffer is a safety net, not a security control). func GetDockerVolumeHeadroom() DockerVolumeHeadroom { di := GetDiskUsage(DockerVolumePath) if di == nil || di.TotalGB <= 0 { return DockerVolumeHeadroom{} } reserve := DockerVolumeReserveGB(di.TotalGB) return DockerVolumeHeadroom{ TotalGB: di.TotalGB, AvailGB: di.AvailGB, ReserveGB: reserve, BelowReserve: di.AvailGB <= reserve, OK: true, } }