v0.58.0: infra-protection prevention layer for the OS/Docker-data split (Phase 2)
Reserved-buffer headroom guard on the Docker-data volume (system/dockervol.go, max(5GB,10%)); deploy-time hard gate refuses (HTTP 507) when below the buffer (api/router.go); deploy page warns + disables the button (deploy.html); runtime disk monitor confirmed to watch the Docker volume above the buffer. Log rotation baked into the golden (agent side). Phase 1 = felhom-agent v0.29.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package system
|
||||
|
||||
import "testing"
|
||||
|
||||
// DockerVolumeReserveGB = max(5 GB, 10% of total): a flat 5 GB floor for small volumes, scaling to
|
||||
// 10% on larger ones (so a 256 GB data volume reserves ~25.6 GB, not the Tier-2 guard's 20%).
|
||||
func TestDockerVolumeReserveGB(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
totalGB float64
|
||||
want float64
|
||||
}{
|
||||
{"tiny volume uses the 5G floor", 16, 5},
|
||||
{"50G volume: 10% = 5G ties the floor", 50, 5},
|
||||
{"100G volume: 10% dominates", 100, 10},
|
||||
{"256G data volume", 256, 25.6},
|
||||
{"zero total still floors at 5G", 0, 5},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := DockerVolumeReserveGB(c.totalGB); got != c.want {
|
||||
t.Errorf("%s: DockerVolumeReserveGB(%.0f) = %.2f, want %.2f", c.name, c.totalGB, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user