Report each anonymous volume's size and the space freed (issue 1796)
mesh/merge-gate head 07b3c5e77a19 merged into trunk f3100d111633 — pass: builds docker → ace, g14, novox, shanks; no bus step; every machine composes with …
mesh/repo-check head 07b3c5e77a19 merged into trunk f3100d111633 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/merge-gate head 07b3c5e77a19 merged into trunk f3100d111633 — pass: builds docker → ace, g14, novox, shanks; no bus step; every machine composes with …
mesh/repo-check head 07b3c5e77a19 merged into trunk f3100d111633 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
This commit is contained in:
@@ -1232,10 +1232,11 @@ func (c *Client) RemoveAnonymousVolumes(ctx context.Context, a RemoveVolumesAsk)
|
|||||||
if !a.DryRun && strings.TrimSpace(a.Why) == "" {
|
if !a.DryRun && strings.TrimSpace(a.Why) == "" {
|
||||||
return nil, fmt.Errorf("why is required when dry_run is false")
|
return nil, fmt.Errorf("why is required when dry_run is false")
|
||||||
}
|
}
|
||||||
all, err := c.Volumes(ctx, false, false)
|
all, err := c.Volumes(ctx, false, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
sizes := map[string]string{}
|
||||||
candidates := []string{}
|
candidates := []string{}
|
||||||
skipped := []map[string]any{}
|
skipped := []map[string]any{}
|
||||||
skip := func(name, why string) { skipped = append(skipped, map[string]any{"name": name, "why": why}) }
|
skip := func(name, why string) { skipped = append(skipped, map[string]any{"name": name, "why": why}) }
|
||||||
@@ -1252,12 +1253,20 @@ func (c *Client) RemoveAnonymousVolumes(ctx context.Context, a RemoveVolumesAsk)
|
|||||||
skip(name, "not created before "+a.CreatedBefore.Format(time.RFC3339)+" (created "+fmt.Sprint(v["created"])+")")
|
skip(name, "not created before "+a.CreatedBefore.Format(time.RFC3339)+" (created "+fmt.Sprint(v["created"])+")")
|
||||||
default:
|
default:
|
||||||
candidates = append(candidates, name)
|
candidates = append(candidates, name)
|
||||||
|
sizes[name] = fmt.Sprint(v["size"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
answer := map[string]any{"dry_run": a.DryRun, "skipped": skipped,
|
sum := func(names []string) int64 {
|
||||||
"space_freed": "not reported: docker volume rm names what it removed; docker_disk_usage shows the space"}
|
var t int64
|
||||||
|
for _, n := range names {
|
||||||
|
t += max(Bytes(sizes[n]), 0)
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
answer := map[string]any{"dry_run": a.DryRun, "skipped": skipped, "sizes": sizes}
|
||||||
if a.DryRun {
|
if a.DryRun {
|
||||||
answer["would_remove"] = candidates
|
answer["would_remove"] = candidates
|
||||||
|
answer["would_free_bytes"] = sum(candidates)
|
||||||
answer["note"] = "nothing was removed: call again with dry_run false and why to remove them"
|
answer["note"] = "nothing was removed: call again with dry_run false and why to remove them"
|
||||||
return answer, nil
|
return answer, nil
|
||||||
}
|
}
|
||||||
@@ -1271,6 +1280,7 @@ func (c *Client) RemoveAnonymousVolumes(ctx context.Context, a RemoveVolumesAsk)
|
|||||||
}
|
}
|
||||||
answer["skipped"] = skipped
|
answer["skipped"] = skipped
|
||||||
answer["removed"], answer["count"], answer["why"] = removed, len(removed), a.Why
|
answer["removed"], answer["count"], answer["why"] = removed, len(removed), a.Why
|
||||||
|
answer["freed_bytes"] = sum(removed)
|
||||||
return answer, nil
|
return answer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -441,6 +441,7 @@ func TestRemoveAnonymousVolumesTakesOnlyTheUnmountedAnonymousOnes(t *testing.T)
|
|||||||
{"Name":"old","Driver":"local","CreatedAt":"2026-09-01T00:00:00Z","Labels":{"com.docker.volume.anonymous":""}},
|
{"Name":"old","Driver":"local","CreatedAt":"2026-09-01T00:00:00Z","Labels":{"com.docker.volume.anonymous":""}},
|
||||||
{"Name":"young","Driver":"local","CreatedAt":"2026-10-04T11:00:00Z","Labels":{"com.docker.volume.anonymous":""}},
|
{"Name":"young","Driver":"local","CreatedAt":"2026-10-04T11:00:00Z","Labels":{"com.docker.volume.anonymous":""}},
|
||||||
{"Name":"named","Driver":"local","CreatedAt":"2026-01-01T00:00:00Z"}]`}).
|
{"Name":"named","Driver":"local","CreatedAt":"2026-01-01T00:00:00Z"}]`}).
|
||||||
|
on("docker system df --verbose", Ran{Stdout: `{"Volumes":[{"Name":"old","Size":"1.5MB"},{"Name":"young","Size":"1kB"}]}`}).
|
||||||
on("docker volume rm old", Ran{Stdout: "old\n"})
|
on("docker volume rm old", Ran{Stdout: "old\n"})
|
||||||
c := client(f, 1000)
|
c := client(f, 1000)
|
||||||
before := time.Date(2026, 10, 3, 0, 0, 0, 0, time.UTC)
|
before := time.Date(2026, 10, 3, 0, 0, 0, 0, time.UTC)
|
||||||
@@ -451,12 +452,15 @@ func TestRemoveAnonymousVolumesTakesOnlyTheUnmountedAnonymousOnes(t *testing.T)
|
|||||||
if f.ran("docker volume rm") || !reflect.DeepEqual(got["would_remove"], []string{"old"}) {
|
if f.ran("docker volume rm") || !reflect.DeepEqual(got["would_remove"], []string{"old"}) {
|
||||||
t.Fatalf("dry run: %v %+v", got, f.calls)
|
t.Fatalf("dry run: %v %+v", got, f.calls)
|
||||||
}
|
}
|
||||||
|
if got["would_free_bytes"] != int64(1500000) || got["sizes"].(map[string]string)["old"] != "1.5MB" {
|
||||||
|
t.Fatalf("dry run size: %v", got)
|
||||||
|
}
|
||||||
skipped := got["skipped"].([]map[string]any)
|
skipped := got["skipped"].([]map[string]any)
|
||||||
why := map[string]string{}
|
why := map[string]string{}
|
||||||
for _, s := range skipped {
|
for _, s := range skipped {
|
||||||
why[s["name"].(string)] = s["why"].(string)
|
why[s["name"].(string)] = s["why"].(string)
|
||||||
}
|
}
|
||||||
if len(why) != 3 || why["webdata"] == "" || why["young"] == "" || why["named"] == "" {
|
if len(why) != 3 || !strings.Contains(why["webdata"], "mesh holds") || why["young"] == "" || why["named"] == "" {
|
||||||
t.Fatalf("skipped: %v", skipped)
|
t.Fatalf("skipped: %v", skipped)
|
||||||
}
|
}
|
||||||
got, err = c.RemoveAnonymousVolumes(context.Background(), RemoveVolumesAsk{Why: "leftovers", CreatedBefore: &before})
|
got, err = c.RemoveAnonymousVolumes(context.Background(), RemoveVolumesAsk{Why: "leftovers", CreatedBefore: &before})
|
||||||
@@ -466,6 +470,9 @@ func TestRemoveAnonymousVolumesTakesOnlyTheUnmountedAnonymousOnes(t *testing.T)
|
|||||||
if !f.ran("docker volume rm old") || got["count"] != 1 || !reflect.DeepEqual(got["removed"], []string{"old"}) {
|
if !f.ran("docker volume rm old") || got["count"] != 1 || !reflect.DeepEqual(got["removed"], []string{"old"}) {
|
||||||
t.Fatalf("removed: %v %+v", got, f.calls)
|
t.Fatalf("removed: %v %+v", got, f.calls)
|
||||||
}
|
}
|
||||||
|
if got["freed_bytes"] != int64(1500000) {
|
||||||
|
t.Fatalf("freed: %v", got)
|
||||||
|
}
|
||||||
for _, c := range f.calls {
|
for _, c := range f.calls {
|
||||||
if len(c.args) > 1 && c.args[0] == "volume" && c.args[1] == "rm" && (len(c.args) != 3 || c.args[2] != "old") {
|
if len(c.args) > 1 && c.args[0] == "volume" && c.args[1] == "rm" && (len(c.args) != 3 || c.args[2] != "old") {
|
||||||
t.Errorf("reached too far: %v", c.args)
|
t.Errorf("reached too far: %v", c.args)
|
||||||
|
|||||||
@@ -291,7 +291,7 @@ func tools(c *Client) []stdio.Tool {
|
|||||||
Name: "docker_volumes_remove_anonymous",
|
Name: "docker_volumes_remove_anonymous",
|
||||||
Description: "Remove the leftover anonymous volumes (novox/hq issue 1796): only a volume docker labels anonymous (as docker_volumes says), which no container mounts in any state " +
|
Description: "Remove the leftover anonymous volumes (novox/hq issue 1796): only a volume docker labels anonymous (as docker_volumes says), which no container mounts in any state " +
|
||||||
"and so none the mesh holds, and with created_before only one created before it. Every other volume is answered with why it stays; a named volume is data and is never taken, " +
|
"and so none the mesh holds, and with created_before only one created before it. Every other volume is answered with why it stays; a named volume is data and is never taken, " +
|
||||||
"and no volume can be named. Each is removed on its own, never forced; the answer has the count and names removed. Docker does not report the space a removed volume freed. " +
|
"and no volume can be named. Each is removed on its own, never forced; the answer has the count and names removed. The answer has each volume's size and the bytes freed (or, in a dry run, that would be). " +
|
||||||
"A dry run by default; dry_run false needs why. Replaces docker volume rm and docker volume prune. (a)",
|
"A dry run by default; dry_run false needs why. Replaces docker volume rm and docker volume prune. (a)",
|
||||||
Input: map[string]any{
|
Input: map[string]any{
|
||||||
"dry_run": map[string]any{"type": "boolean", "description": "list only (default true)"},
|
"dry_run": map[string]any{"type": "boolean", "description": "list only (default true)"},
|
||||||
|
|||||||
Reference in New Issue
Block a user