fix(ansible): the swarm prune reads every image from Docker's API, untagged ones included (#6399)
`docker image ls` on Docker 29 leaves out images with no tag, which is every image a swarm task pulled by digest: the check run on VDNT-Docker03 saw 4 of its 58. Missing them only kept them, but superseded digests there would never have gone. The list now comes from GET /images/json on the socket. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,11 +16,14 @@ Stdlib only; shipped beside the playbook in Steward's built-in source.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import http.client
|
||||
import json
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
DEADLINE_SECONDS = 300
|
||||
DOCKER_SOCKET = "/var/run/docker.sock"
|
||||
|
||||
|
||||
def docker(*args: str) -> str:
|
||||
@@ -31,6 +34,34 @@ def docker(*args: str) -> str:
|
||||
return done.stdout
|
||||
|
||||
|
||||
class _UnixConnection(http.client.HTTPConnection):
|
||||
def __init__(self, path: str):
|
||||
super().__init__("localhost", timeout=DEADLINE_SECONDS)
|
||||
self.unix_path = path
|
||||
|
||||
def connect(self):
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.settimeout(self.timeout)
|
||||
sock.connect(self.unix_path)
|
||||
self.sock = sock
|
||||
|
||||
|
||||
def api(path: str):
|
||||
"""A GET on Docker's own API. The image list is read here, not from
|
||||
`docker image ls`, which leaves out images with no tag: every image a
|
||||
swarm task pulled by digest."""
|
||||
conn = _UnixConnection(DOCKER_SOCKET)
|
||||
try:
|
||||
conn.request("GET", path)
|
||||
resp = conn.getresponse()
|
||||
body = resp.read()
|
||||
finally:
|
||||
conn.close()
|
||||
if resp.status != 200:
|
||||
raise RuntimeError(f"docker API {path}: HTTP {resp.status}")
|
||||
return json.loads(body)
|
||||
|
||||
|
||||
def short(repo: str) -> str:
|
||||
"""A repository the way Docker lists it locally: no docker.io/ or library/."""
|
||||
for prefix in ("docker.io/", "index.docker.io/"):
|
||||
@@ -54,7 +85,7 @@ def wanted(service_images) -> tuple[set, set]:
|
||||
|
||||
|
||||
def removable(images: list, digests: set, refs: set, used: set) -> list:
|
||||
"""The images (`docker image inspect` rows) no container uses and no
|
||||
"""The images (Docker API /images/json rows) no container uses and no
|
||||
service is pinned to."""
|
||||
out = []
|
||||
for im in images:
|
||||
@@ -80,9 +111,8 @@ def main(argv: list) -> int:
|
||||
services = json.loads(docker("service", "inspect", *ids)) if ids else []
|
||||
ids = docker("ps", "-aq", "--no-trunc").split()
|
||||
containers = json.loads(docker("container", "inspect", *ids)) if ids else []
|
||||
image_ids = sorted(set(docker("image", "ls", "-q", "--no-trunc").split()))
|
||||
images = json.loads(docker("image", "inspect", *image_ids)) if image_ids else []
|
||||
except (RuntimeError, ValueError, OSError, subprocess.TimeoutExpired) as exc:
|
||||
images = api("/images/json")
|
||||
except (RuntimeError, ValueError, OSError, subprocess.TimeoutExpired, http.client.HTTPException) as exc:
|
||||
print(json.dumps({"error": f"nothing removed: {exc}"}))
|
||||
return 2
|
||||
# A container's image is kept by its id and by the reference it was made
|
||||
|
||||
Reference in New Issue
Block a user