The volume prune gets the same treatment as the image prune in 0.61.0:
the candidates are listed and removed one by one in the background, and
the Volumes page shows a progress bar (x / y, the volume being removed,
failures) followed by a toast with the space freed.
The button also used to delete without asking, and a bare docker volume
prune only removes anonymous volumes on Engine 23+ with no way to include
named ones. It now opens a confirmation dialog with an explicit -a option
that warns it also takes the data of every stack that is down.
- Candidates: volumes no container (running or stopped) mounts, local
scope only, and anonymous (com.docker.volume.anonymous) unless -a.
Volumes from pre-23 daemons carry no such label, so they only go with
-a, which errs on the side of keeping data.
- Removed without force, so a volume mounted since the listing is
refused; one refusal does not stop the rest.
- No closing daemon prune: nothing is left over, and on a pre-23 daemon an
unfiltered one would delete named volumes too.
- Freed space is the sum of the deleted volumes' sizes (volumes share no
data), from the size cache, which is invalidated afterwards.
- POST /api/volumes/prune takes all= and background=; without background
it still waits and returns {VolumesDeleted, SpaceReclaimed}.
GET /api/volumes/prune-status is admin-only.
- volume_sizes() ("Compute sizes") now uses the long-timeout client; it
walks every volume and could hit the same 60s limit.
The progress machinery moved into services/prune_job.py (PruneJob,
PrunePlan) and, on the frontend, the usePruneJob hook and PruneProgress
component, shared by both pages. The status now carries a phase
(preparing / removing / finishing) so the parts without a count say what
they are doing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
85 lines
2.5 KiB
Python
85 lines
2.5 KiB
Python
"""Slow Docker calls must fail with a message, not a bare 500.
|
|
|
|
A volume or image prune that deletes a lot of data easily runs past docker-py's
|
|
60s read timeout. requests' ``ReadTimeout`` is not a ``DockerException``, so it
|
|
slipped past ``safe_call`` and the UI only showed "Request failed with status
|
|
code 500". Prunes now use a client with a long timeout, and any transport
|
|
timeout that still happens is turned into a ``DockerError`` (-> 502 + detail).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
import requests
|
|
|
|
|
|
def test_safe_call_wraps_read_timeout():
|
|
from docker_client import DockerError, safe_call
|
|
|
|
def slow():
|
|
raise requests.exceptions.ReadTimeout("Read timed out. (read timeout=60)")
|
|
|
|
with pytest.raises(DockerError) as info:
|
|
safe_call(slow)
|
|
assert info.value.error == "docker_timeout"
|
|
|
|
|
|
def test_safe_call_wraps_connection_error():
|
|
from docker_client import DockerError, safe_call
|
|
|
|
def gone():
|
|
raise requests.exceptions.ConnectionError("socket went away")
|
|
|
|
with pytest.raises(DockerError) as info:
|
|
safe_call(gone)
|
|
assert info.value.error == "docker_unavailable"
|
|
|
|
|
|
def test_volume_prune_timeout_is_a_502_with_detail(as_admin, monkeypatch):
|
|
from services import volume_service
|
|
|
|
class Api:
|
|
def containers(self, all=False):
|
|
raise requests.exceptions.ReadTimeout("Read timed out.")
|
|
|
|
class Client:
|
|
api = Api()
|
|
|
|
monkeypatch.setattr(volume_service, "get_long_client", lambda: Client())
|
|
r = as_admin.post("/api/volumes/prune")
|
|
assert r.status_code == 502
|
|
assert r.json()["error"] == "docker_timeout"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"module, fn, args",
|
|
[
|
|
("network_service", "prune_networks", ()),
|
|
],
|
|
)
|
|
def test_prunes_use_the_long_timeout_client(monkeypatch, module, fn, args):
|
|
import importlib
|
|
|
|
svc = importlib.import_module(f"services.{module}")
|
|
used = []
|
|
|
|
class Any_:
|
|
def prune(self, filters=None):
|
|
used.append("long")
|
|
return {}
|
|
|
|
class Client:
|
|
volumes = images = networks = Any_()
|
|
|
|
def short():
|
|
raise AssertionError("prune must not use the 60s client")
|
|
|
|
monkeypatch.setattr(svc, "get_client", short)
|
|
monkeypatch.setattr(svc, "get_long_client", lambda: Client())
|
|
getattr(svc, fn)(*args)
|
|
assert used == ["long"]
|
|
|
|
|
|
# The image and volume prunes go item by item and have their own tests in
|
|
# test_image_prune.py / test_volume_prune.py, including that they use the
|
|
# long-timeout client.
|