Docker: delete volumes nothing uses, prune anonymous ones; images marked in use, remove unused
A named volume is deleted by typing its name, one at a time; the API wants it named twice; nothing is forced. Prune touches anonymous volumes only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
c017b422d5
commit
634e286796
+88
-6
@@ -135,10 +135,44 @@ func (s *Server) dockerOverview(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
return rows
|
||||
}
|
||||
// Who uses what, from one inspect of every container: an image or a volume nothing uses is
|
||||
// the only kind this page offers to remove.
|
||||
imageUsed, volumeUsers := map[string]bool{}, map[string][]string{}
|
||||
if ids, err := dockerRun(ctx, "ps", "-aq"); err == nil && len(strings.Fields(string(ids))) > 0 {
|
||||
const f = `{{.Name}}|{{.Image}}|{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}},{{end}}{{end}}`
|
||||
if out, err := dockerRun(ctx, append([]string{"inspect", "-f", f}, strings.Fields(string(ids))...)...); err == nil {
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||
p := strings.SplitN(line, "|", 3)
|
||||
if len(p) < 3 {
|
||||
continue
|
||||
}
|
||||
if id := strings.TrimPrefix(p[1], "sha256:"); len(id) >= 12 {
|
||||
imageUsed[id[:12]] = true
|
||||
}
|
||||
for _, v := range strings.Split(p[2], ",") {
|
||||
if v != "" {
|
||||
volumeUsers[v] = append(volumeUsers[v], strings.TrimPrefix(p[0], "/"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
images := list("images", "--format", "{{json .}}")
|
||||
for _, im := range images {
|
||||
im["in_use"] = imageUsed[dstr(im, "ID")]
|
||||
}
|
||||
volumes := list("volume", "ls", "--format", "{{json .}}")
|
||||
for _, v := range volumes {
|
||||
users := volumeUsers[dstr(v, "Name")]
|
||||
if users == nil {
|
||||
users = []string{}
|
||||
}
|
||||
sort.Strings(users)
|
||||
v["used_by"] = users
|
||||
v["anonymous"] = strings.Contains(dstr(v, "Labels"), "com.docker.volume.anonymous")
|
||||
}
|
||||
jsonOK(w, 200, map[string]any{"available": true, "tool": dockerTool(), "containers": containers,
|
||||
"images": list("images", "--format", "{{json .}}"),
|
||||
"volumes": list("volume", "ls", "--format", "{{json .}}"),
|
||||
"networks": list("network", "ls", "--format", "{{json .}}")})
|
||||
"images": images, "volumes": volumes, "networks": list("network", "ls", "--format", "{{json .}}")})
|
||||
}
|
||||
|
||||
var dockerRef = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$`)
|
||||
@@ -203,11 +237,18 @@ func (s *Server) imageRemove(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(204)
|
||||
}
|
||||
|
||||
// imagePrune removes dangling images only — layers no tag points at any more, left behind by pulls.
|
||||
// imagePrune removes dangling images — layers no tag points at any more, left behind by pulls —
|
||||
// or, asked for {"all": true}, every image no container uses. Those can all be pulled again.
|
||||
func (s *Server) imagePrune(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Minute)
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute)
|
||||
defer cancel()
|
||||
out, err := dockerRun(ctx, "image", "prune", "-f")
|
||||
var body struct{ All bool }
|
||||
_ = json.NewDecoder(io.LimitReader(r.Body, 4096)).Decode(&body)
|
||||
args := []string{"image", "prune", "-f"}
|
||||
if body.All {
|
||||
args = append(args, "-a")
|
||||
}
|
||||
out, err := dockerRun(ctx, args...)
|
||||
if err != nil {
|
||||
jsonErr(w, 500, err.Error())
|
||||
return
|
||||
@@ -312,3 +353,44 @@ func (s *Server) dockerInspect(w http.ResponseWriter, r *http.Request) {
|
||||
"compose_file": dig(c, "Config", "Labels", "com.docker.compose.project.config_files"),
|
||||
})
|
||||
}
|
||||
|
||||
var volumeRef = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,254}$`)
|
||||
|
||||
// volumeRemove deletes one volume and what is in it, which nothing brings back. So it is never
|
||||
// forced (Docker refuses a volume a container still uses), it is one at a time, and the request
|
||||
// has to name the volume twice — a slip of a script can't delete one by accident.
|
||||
func (s *Server) volumeRemove(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct{ Name, Confirm string }
|
||||
if err := json.NewDecoder(io.LimitReader(r.Body, 4096)).Decode(&body); err != nil || !volumeRef.MatchString(body.Name) {
|
||||
jsonErr(w, 400, "which volume?")
|
||||
return
|
||||
}
|
||||
if body.Confirm != body.Name {
|
||||
jsonErr(w, 400, "name the volume again in \"confirm\" to delete it")
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 90*time.Second)
|
||||
defer cancel()
|
||||
if _, err := dockerRun(ctx, "volume", "rm", body.Name); err != nil {
|
||||
jsonErr(w, 409, err.Error())
|
||||
return
|
||||
}
|
||||
log.Printf("docker: volume rm %s (from %s)", body.Name, clientIP(r))
|
||||
w.WriteHeader(204)
|
||||
}
|
||||
|
||||
// volumePrune removes unused *anonymous* volumes only: the nameless ones an image's VOLUME line
|
||||
// leaves behind each time its container is recreated. Named volumes are somebody's data, and a
|
||||
// stack that is merely down leaves its volumes "unused" — those go one at a time, by name.
|
||||
func (s *Server) volumePrune(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute)
|
||||
defer cancel()
|
||||
out, err := dockerRun(ctx, "volume", "prune", "-f")
|
||||
if err != nil {
|
||||
jsonErr(w, 500, err.Error())
|
||||
return
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||
log.Printf("docker: volume prune (from %s)", clientIP(r))
|
||||
jsonOK(w, 200, map[string]string{"result": lines[len(lines)-1]})
|
||||
}
|
||||
|
||||
@@ -136,6 +136,8 @@ func New(cfg *config.Config, sessions *auth.Sessions, mgr *term.Manager, col *me
|
||||
m.HandleFunc("POST /api/docker/images/pull", s.auth(s.imagePull))
|
||||
m.HandleFunc("POST /api/docker/images/remove", s.auth(s.imageRemove))
|
||||
m.HandleFunc("POST /api/docker/images/prune", s.auth(s.imagePrune))
|
||||
m.HandleFunc("POST /api/docker/volumes/remove", s.auth(s.volumeRemove))
|
||||
m.HandleFunc("POST /api/docker/volumes/prune", s.auth(s.volumePrune))
|
||||
m.HandleFunc("GET /api/docker/stacks/{name}", s.auth(s.stackFile))
|
||||
m.HandleFunc("PUT /api/docker/stacks/{name}", s.auth(s.stackSave))
|
||||
m.HandleFunc("POST /api/docker/stacks/{name}/{action}", s.auth(s.stackAction))
|
||||
|
||||
Reference in New Issue
Block a user