v0.294.0: off-site clean-up guard follows the policy's own constants (R-867); no image clean-up while compose pulls (R-863); stderr tail (R-864); move-aside destination logged (R-869)
gates / gates (push) Successful in 28s
gates / gates (push) Successful in 28s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
package dockerexec
|
||||
|
||||
import "sync"
|
||||
|
||||
// ── R-863 (v0.294.0): no image clean-up while an image is being pulled ───────────────────────────────
|
||||
//
|
||||
// MEASURED 2026-10-04 night drill, a fresh box: the controller's one-time image clean-up ran while the
|
||||
// household's first BookStack install was inside `docker compose up -d`. compose had pulled
|
||||
// `mariadb@sha256:…` (stored untagged) and not yet created the container, so no container, installed app
|
||||
// or undo named it; the clean-up deleted it one second before compose created the container, and the
|
||||
// install failed. An app being installed is not "installed" yet, and a restore or undo pulls the same way.
|
||||
//
|
||||
// THE RULE: every compose command that can pull an image and then create a container from it (up, pull,
|
||||
// create, run) holds the image-work lock SHARED for its whole run; an image clean-up pass takes it
|
||||
// EXCLUSIVELY, without waiting (TryLock). So a pass never runs while any such command runs, and a command
|
||||
// that starts during a pass waits the few seconds the pass takes. A pass that cannot take the lock does
|
||||
// not run; its caller tries again later. Pinned by TestR863_* (internal/stacks/image_retention_r863_test.go).
|
||||
|
||||
var imageWorkMu sync.RWMutex
|
||||
|
||||
// ImagePulling reports whether compose args are an image-pulling verb (up, pull, create, run). Flags
|
||||
// before the verb (`-p name`, `--profile x`) are skipped.
|
||||
func ImagePulling(args []string) bool {
|
||||
for i := 0; i < len(args); i++ {
|
||||
a := args[i]
|
||||
if a == "compose" {
|
||||
continue
|
||||
}
|
||||
if len(a) > 0 && a[0] == '-' {
|
||||
switch a {
|
||||
case "-p", "--project-name", "-f", "--file", "--profile", "--env-file", "--project-directory":
|
||||
i++ // the flag's value
|
||||
}
|
||||
continue
|
||||
}
|
||||
switch a {
|
||||
case "up", "pull", "create", "run":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// BeginImageWork holds the image-work lock shared while compose args pull images; the returned func
|
||||
// releases it. For any other verb it returns a no-op.
|
||||
func BeginImageWork(args []string) (end func()) {
|
||||
if !ImagePulling(args) {
|
||||
return func() {}
|
||||
}
|
||||
imageWorkMu.RLock()
|
||||
return imageWorkMu.RUnlock
|
||||
}
|
||||
|
||||
// TryImageCleanup takes the image-work lock exclusively if no image work runs now. ok=false: something is
|
||||
// pulling — do not clean up now.
|
||||
func TryImageCleanup() (end func(), ok bool) {
|
||||
if !imageWorkMu.TryLock() {
|
||||
return nil, false
|
||||
}
|
||||
return imageWorkMu.Unlock, true
|
||||
}
|
||||
Reference in New Issue
Block a user