22import kr8s
33import logging
44import os
5+ import re
56import shutil
67import socket
78import tempfile
1617from grpclib .const import Status
1718from grpclib .server import Server
1819from importlib import metadata
20+ from kr8s .asyncio .objects import Pod
1921from pathlib import Path
22+ from typing import Any , Iterator
2023
2124logger = logging .getLogger ("nix-csi" )
2225
4851NAMESPACE = os .environ .get ("KUBE_NAMESPACE" , "nix-csi" )
4952BUILDERS_SERVICE = "nix-csi-builders"
5053
54+ # Nix base32 excludes: e, o, t, u
55+ STORE_PATH_RE = re .compile (r"/nix/store/[0-9a-df-np-sv-z]{32}-[^\s/]+" )
56+
57+
58+ def extract_store_paths (value : Any ) -> Iterator [str ]:
59+ match value :
60+ case str ():
61+ yield from Path (STORE_PATH_RE .findall (value ))
62+ case dict ():
63+ for v in value .values ():
64+ yield from Path (extract_store_paths (v ))
65+ case list () | tuple ():
66+ for item in value :
67+ yield from Path (extract_store_paths (item ))
68+
69+
70+ def extract_store_name (value : Path | str ) -> str :
71+ return str (value ).removeprefix ("/nix/store/" )
72+
73+
5174async def get_current_system ():
5275 """Get system string evaluated by nix"""
5376 return (
@@ -80,6 +103,7 @@ async def get_builder_uris():
80103 logger .warning (f"Failed to discover builder pods: { e } " )
81104 return []
82105
106+
83107class NodeServicer (csi_grpc .NodeBase ):
84108 volumeLocks : defaultdict [str , Semaphore ] = defaultdict (Semaphore )
85109
@@ -102,8 +126,8 @@ async def NodePublishVolume(self, stream):
102126 # Using sentinel path instead of None to avoid Optional type and None checks everywhere.
103127 # The if/elif blocks below should set this to a real path; the exists() check at line ~157
104128 # will fail if none of the branches executed (sentinel path doesn't exist).
105- packagePath : Path = Path ("/nonexistent/path/that/should/never/exist" )
106- gcPath = CSI_GCROOTS / request .volume_id
129+ primaryPackagePath : Path = Path ("/nonexistent/path/that/should/never/exist" )
130+ gcRoot = CSI_GCROOTS / request .volume_id
107131
108132 extraArgs = []
109133
@@ -135,27 +159,57 @@ async def NodePublishVolume(self, stream):
135159 except (GRPCError , OSError , asyncio .TimeoutError ):
136160 logger .warning ("Cache connectivity check failed" )
137161
138- # Source selection order (intentional, documented in README):
139- # 1. storePath - if present, use directly
140- # 2. flakeRef - if storePath not present, build flake
141- # 3. nixExpr - if neither above present, evaluate expression
142- # Users can specify multiple; first non-None in priority order is used.
143- if storePath is not None :
144- async with self .volumeLocks [storePath ]:
145- logger .debug (f"{ storePath = } " )
146- packagePath = Path (storePath )
147- if not packagePath .exists ():
148- await try_console (
162+ podName = request .volume_context .get ("csi.storage.k8s.io/pod.name" )
163+ podNamespace = request .volume_context .get (
164+ "csi.storage.k8s.io/pod.namespace"
165+ )
166+ podUid = request .volume_context .get ("csi.storage.k8s.io/pod.uid" )
167+
168+ packagePaths = []
169+ if podName and podNamespace and podUid :
170+ pod = await Pod .get (podName , podNamespace )
171+ if pod .metadata .uid != podUid :
172+ raise GRPCError (Status .INTERNAL , "poduid doesn't match" )
173+
174+ for packagePath in set (extract_store_paths (pod .raw )):
175+ async with self .volumeLocks [storePath ]:
176+ name = extract_store_name (packagePath )
177+ result = await try_console (
149178 "nix" ,
150179 "build" ,
151180 * extraArgs ,
152181 "--print-out-paths" ,
153182 "--out-link" ,
154- gcPath ,
183+ gcRoot / name ,
155184 packagePath ,
156185 timeout = NIX_BUILD_TIMEOUT ,
157186 )
158- if flakeRef is not None and not packagePath .exists ():
187+ packagePaths .append (Path (result .stdout .splitlines ()[0 ]))
188+
189+ # Source selection order (intentional, documented in README):
190+ # 1. storePath - if present, use directly
191+ # 2. flakeRef - if storePath not present, build flake
192+ # 3. nixExpr - if neither above present, evaluate expression
193+ # Users can specify multiple; first non-None in priority order is used.
194+ if storePath is not None :
195+ async with self .volumeLocks [storePath ]:
196+ logger .debug (f"{ storePath = } " )
197+ name = extract_store_name (storePath )
198+ result = await try_console (
199+ "nix" ,
200+ "build" ,
201+ * extraArgs ,
202+ "--print-out-paths" ,
203+ "--out-link" ,
204+ gcRoot / storePath ,
205+ storePath ,
206+ timeout = NIX_BUILD_TIMEOUT ,
207+ )
208+ packagePaths .append (Path (result .stdout .splitlines ()[0 ]))
209+ if not primaryPackagePath .exists ():
210+ primaryPackagePath = Path (result .stdout .splitlines ()[0 ])
211+
212+ if flakeRef is not None :
159213 async with self .volumeLocks [flakeRef ]:
160214 logger .debug (f"{ flakeRef = } " )
161215
@@ -166,12 +220,15 @@ async def NodePublishVolume(self, stream):
166220 * extraArgs ,
167221 "--print-out-paths" ,
168222 "--out-link" ,
169- gcPath ,
223+ gcRoot / "flake" ,
170224 flakeRef ,
171225 timeout = NIX_BUILD_TIMEOUT ,
172226 )
173- packagePath = Path (result .stdout .splitlines ()[0 ])
174- if nixExpr is not None and not packagePath .exists ():
227+ packagePaths .append (Path (result .stdout .splitlines ()[0 ]))
228+ if not primaryPackagePath .exists ():
229+ primaryPackagePath = Path (result .stdout .splitlines ()[0 ])
230+
231+ if nixExpr is not None :
175232 async with self .volumeLocks [nixExpr ]:
176233 logger .debug (f"{ nixExpr = } " )
177234 with tempfile .NamedTemporaryFile (mode = "w" , suffix = ".nix" ) as tmp :
@@ -185,14 +242,16 @@ async def NodePublishVolume(self, stream):
185242 * extraArgs ,
186243 "--print-out-paths" ,
187244 "--out-link" ,
188- gcPath ,
245+ gcRoot / "expr" ,
189246 "--file" ,
190247 tmp .name ,
191248 timeout = NIX_BUILD_TIMEOUT ,
192249 )
193- packagePath = Path (result .stdout .splitlines ()[0 ])
250+ packagePaths .append (Path (result .stdout .splitlines ()[0 ]))
251+ if not primaryPackagePath .exists ():
252+ primaryPackagePath = Path (result .stdout .splitlines ()[0 ])
194253
195- if not packagePath .exists ():
254+ if not primaryPackagePath .exists ():
196255 logger .error ("packagePath doesn't exist after building" )
197256 raise GRPCError (
198257 Status .INVALID_ARGUMENT ,
@@ -207,13 +266,13 @@ async def NodePublishVolume(self, stream):
207266 # Create NIX_STATE_DIR where database will be initialized
208267 NIX_STATE_DIR .mkdir (parents = True , exist_ok = True )
209268
210- # Get closure
211- paths = (
269+ # Get storepaths from all packages
270+ storePaths = (
212271 await try_captured (
213272 "nix" ,
214273 "path-info" ,
215274 "--recursive" ,
216- packagePath ,
275+ * packagePaths ,
217276 )
218277 ).stdout .splitlines ()
219278
@@ -222,14 +281,16 @@ async def NodePublishVolume(self, stream):
222281 # extra steps. (Hardlinking instead of dumbcopying)
223282
224283 # Install CSI gcroots
225- await try_captured (
226- "nix" ,
227- "build" ,
228- "--out-link" ,
229- gcPath ,
230- packagePath ,
231- timeout = NIX_BUILD_TIMEOUT ,
232- )
284+ for packagePath in packagePaths :
285+ name = extract_store_name (packagePath )
286+ await try_captured (
287+ "nix" ,
288+ "build" ,
289+ "--out-link" ,
290+ gcRoot / name ,
291+ packagePath ,
292+ timeout = NIX_BUILD_TIMEOUT ,
293+ )
233294
234295 # Copy closure to substore, rsync saves a lot of implementation
235296 # headache here. --archive keeps all attributes, --hard-links
@@ -242,7 +303,7 @@ async def NodePublishVolume(self, stream):
242303 "--links" ,
243304 "--hard-links" ,
244305 "--mkpath" ,
245- * paths ,
306+ * storePaths ,
246307 volumeRoot / "nix/store" ,
247308 )
248309
@@ -251,21 +312,23 @@ async def NodePublishVolume(self, stream):
251312 await try_captured (
252313 "nix_init_db" ,
253314 NIX_STATE_DIR ,
254- * paths ,
315+ * storePaths ,
255316 )
256317
257- # install gcroots in container using chroot store this is
318+ # install gcroots in container using chroot, store this is
258319 # required because the auto roots created for /nix/var/result
259320 # will point to Narnia while this one points into store.
260- await try_captured (
261- "nix" ,
262- "build" ,
263- "--store" ,
264- volumeRoot ,
265- "--out-link" ,
266- NIX_STATE_DIR / "gcroots/result" ,
267- packagePath ,
268- )
321+ for packagePath in packagePaths :
322+ name = extract_store_name (packagePath )
323+ await try_captured (
324+ "nix" ,
325+ "build" ,
326+ "--store" ,
327+ volumeRoot ,
328+ "--out-link" ,
329+ NIX_STATE_DIR / f"gcroots/result/{ name } " ,
330+ packagePath ,
331+ )
269332
270333 # install /nix/var/result in container using chroot store
271334 await try_captured (
@@ -275,12 +338,12 @@ async def NodePublishVolume(self, stream):
275338 volumeRoot ,
276339 "--out-link" ,
277340 volumeRoot / "nix/var/result" ,
278- packagePath ,
341+ primaryPackagePath ,
279342 )
280343 except Exception :
281344 logger .exception ("Failed to build volume" )
282345 # Remove gcroots if we failed something else
283- gcPath . unlink ( missing_ok = True )
346+ shutil . rmtree ( gcRoot , ignore_errors = True )
284347 # Remove what we were working on
285348 shutil .rmtree (volumeRoot , True )
286349 raise
@@ -297,7 +360,7 @@ async def NodePublishVolume(self, stream):
297360 "--bind" ,
298361 "-o" ,
299362 "ro" ,
300- volumeRoot / "nix" ,
363+ volumeRoot ,
301364 targetPath ,
302365 ]
303366 else :
@@ -315,7 +378,7 @@ async def NodePublishVolume(self, stream):
315378 "overlay" ,
316379 "overlay" ,
317380 "-o" ,
318- f"rw,lowerdir={ volumeRoot / 'nix' } ,upperdir={ upperdir } ,workdir={ workdir } " ,
381+ f"rw,lowerdir={ volumeRoot } ,upperdir={ upperdir } ,workdir={ workdir } " ,
319382 targetPath ,
320383 ]
321384
@@ -324,7 +387,7 @@ async def NodePublishVolume(self, stream):
324387 logger .debug (f"Mount target { targetPath } was already mounted" )
325388 elif mount .returncode != 0 :
326389 # Clean up resources on mount failure
327- gcPath . unlink ( missing_ok = True )
390+ shutil . rmtree ( gcRoot , ignore_errors = True )
328391 shutil .rmtree (volumeRoot , ignore_errors = True )
329392 raise GRPCError (
330393 Status .INTERNAL ,
@@ -334,7 +397,7 @@ async def NodePublishVolume(self, stream):
334397 reply = csi_pb2 .NodePublishVolumeResponse ()
335398 await stream .send_message (reply )
336399
337- task = asyncio .create_task (copyToCache (packagePath ))
400+ task = asyncio .create_task (copyToCache (primaryPackagePath ))
338401 task .add_done_callback (
339402 lambda t : logger .error (f"copyToCache failed: { t .exception ()} " )
340403 if t .exception ()
@@ -385,11 +448,11 @@ async def NodeUnpublishVolume(self, stream):
385448 )
386449
387450 # Remove gcroots
388- gcPath = CSI_GCROOTS / request .volume_id
389- if gcPath .exists ():
451+ gcRoot = CSI_GCROOTS / request .volume_id
452+ if gcRoot .exists ():
390453 try :
391- gcPath . unlink ( )
392- logger .debug (f"unlinked { gcPath = } " )
454+ shutil . rmtree ( gcRoot , ignore_errors = True )
455+ logger .debug (f"unlinked { gcRoot = } " )
393456 except Exception as ex :
394457 raise GRPCError (
395458 Status .INTERNAL , f"unlinking { targetPath = } failed" , ex
0 commit comments