Skip to content

Commit 0a953f3

Browse files
Lillecarlclaude
andcommitted
Implement NRI container garbage collection in StopContainer
Add sweeping garbage collection that removes stale NRI volumes when containers stop. The plugin now caches the CRI socket at initialization and uses it to query the runtime for active containers, then removes volumes for containers no longer in the active list. Changes: - Cache CRI socket in NriPlugin constructor (required for GC) - Query active containers from CRI API in StopContainer hook - Remove the stopping container from active list - Clean up volumes in NRI_CONTAINERS that aren't in active list - Log GC progress and stale volume removals This ensures orphaned volumes are cleaned up even if containers are removed via external tools or fail to call StopContainer. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent f8d9750 commit 0a953f3

3 files changed

Lines changed: 53 additions & 9 deletions

File tree

kubenix/rbac.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ in
2222
"list"
2323
];
2424
}
25-
# Query kubelet configz via API server proxy for CRI socket discovery
25+
# Kubelet configz via API server proxy for CRI socket discovery
2626
{
2727
apiGroups = [ "" ];
2828
resources = [ "nodes/proxy" ];

python/nix_csi/cri.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,18 @@ async def get_cri_socket() -> Path:
2828
raise RuntimeError("KUBE_NODE_NAME environment variable not set")
2929

3030
try:
31-
api = kr8s.asyncio.Api()
31+
api = await kr8s.asyncio.api()
3232
async with api.call_api(
3333
"GET",
34-
url=f"/api/v1/nodes/{node_name}/proxy/configz",
34+
url=f"nodes/{node_name}/proxy/configz",
3535
) as response:
3636
config = response.json()
3737
endpoint = config.get("kubeletconfig", {}).get("containerRuntimeEndpoint")
3838

3939
if not endpoint:
40-
raise RuntimeError("containerRuntimeEndpoint not found in kubelet configz")
40+
raise RuntimeError(
41+
"containerRuntimeEndpoint not found in kubelet configz"
42+
)
4143

4244
# Strip unix:// prefix if present
4345
endpoint = endpoint.removeprefix("unix://")

python/nix_csi/nriplugin.py

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
NRI_PLUGIN_NAME,
3131
NRI_RUNTIME_SOCKET,
3232
)
33+
from .cri import get_cri_socket, list_container_ids
3334
from .nix import build_packages, get_build_args, get_closure_paths
3435
from .ns_mount import mount_in_container
3536
from .store import extract_store_paths
@@ -121,9 +122,10 @@ def parse_store_mounts(pod_annotations, container_name: str) -> dict[Path, Path]
121122
class NriPlugin(nri_grpc.PluginBase):
122123
"""NRI plugin with ZeroMQ build coordination."""
123124

124-
def __init__(self, zmq_server: ZeroMQServer):
125+
def __init__(self, zmq_server: ZeroMQServer, cri_socket: Path):
125126
super().__init__()
126127
self.zmq_server = zmq_server
128+
self.cri_socket = cri_socket
127129
# Find nri-wait binary on PATH (available as nix-csi dependency)
128130
self.nri_wait_bin = shutil.which("wait")
129131
logger.debug("nri-wait binary resolved to: %s", self.nri_wait_bin)
@@ -312,11 +314,10 @@ async def StopContainer(self, stream) -> None:
312314
req.container.name,
313315
)
314316

315-
# Phase 1: Cleanup volume directory if it was created
316317
container_id = req.container.id
317-
# Use pod-side path for cleanup (same as creation)
318-
volume_path = NRI_CONTAINERS / container_id
319318

319+
# Phase 1: Cleanup volume directory for this container
320+
volume_path = NRI_CONTAINERS / container_id
320321
if volume_path.exists():
321322
try:
322323
shutil.rmtree(volume_path)
@@ -328,6 +329,44 @@ async def StopContainer(self, stream) -> None:
328329
e,
329330
)
330331

332+
# Phase 2: Garbage collect stale volumes from containers no longer in CRI
333+
try:
334+
# Get list of active containers from CRI, excluding the one stopping
335+
# Access socket through host mount since we're in a container
336+
socket_path = Path("/host") / self.cri_socket.relative_to("/")
337+
active_ids = await list_container_ids(socket_path)
338+
active_ids.discard(container_id) # Remove the stopping container
339+
logger.debug(
340+
"GC: Active containers from CRI: %d (excluding stopping container)",
341+
len(active_ids),
342+
)
343+
344+
# Clean up volumes for containers not in active list
345+
if NRI_CONTAINERS.exists():
346+
stale_count = 0
347+
for volume_dir in NRI_CONTAINERS.iterdir():
348+
if volume_dir.is_dir() and volume_dir.name not in active_ids:
349+
try:
350+
shutil.rmtree(volume_dir)
351+
stale_count += 1
352+
logger.debug(
353+
"GC: Removed stale volume for container=%r",
354+
volume_dir.name,
355+
)
356+
except Exception as e:
357+
logger.warning(
358+
"GC: Failed to remove stale volume at %r: %s",
359+
volume_dir,
360+
e,
361+
)
362+
if stale_count > 0:
363+
logger.info("GC: Cleaned up %d stale NRI volumes", stale_count)
364+
except Exception as e:
365+
logger.warning(
366+
"GC: Failed to perform garbage collection: %s",
367+
e,
368+
)
369+
331370
await stream.send_message(nri_pb2.StopContainerResponse())
332371

333372
async def UpdatePodSandbox(self, stream) -> None:
@@ -582,8 +621,11 @@ async def _nri_run() -> None:
582621
zmq_server = ZeroMQServer()
583622
await zmq_server.initialize()
584623

624+
# Discover CRI socket for garbage collection
625+
cri_socket = await get_cri_socket()
626+
585627
mapping: dict = {}
586-
plugin = NriPlugin(zmq_server)
628+
plugin = NriPlugin(zmq_server, cri_socket)
587629
for h in [plugin]:
588630
mapping.update(h.__mapping__())
589631

0 commit comments

Comments
 (0)