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 : Any ) -> Iterator [str ]:
71+ for path in extract_store_paths (value ):
72+ yield str (path ).removeprefix ("/nix/store/" )
73+
74+
5175async def get_current_system ():
5276 """Get system string evaluated by nix"""
5377 return (
@@ -80,6 +104,7 @@ async def get_builder_uris():
80104 logger .warning (f"Failed to discover builder pods: { e } " )
81105 return []
82106
107+
83108class NodeServicer (csi_grpc .NodeBase ):
84109 volumeLocks : defaultdict [str , Semaphore ] = defaultdict (Semaphore )
85110
@@ -102,8 +127,8 @@ async def NodePublishVolume(self, stream):
102127 # Using sentinel path instead of None to avoid Optional type and None checks everywhere.
103128 # The if/elif blocks below should set this to a real path; the exists() check at line ~157
104129 # 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
130+ primaryPackagePath : Path = Path ("/nonexistent/path/that/should/never/exist" )
131+ gcRoot = CSI_GCROOTS / request .volume_id
107132
108133 extraArgs = []
109134
@@ -135,27 +160,57 @@ async def NodePublishVolume(self, stream):
135160 except (GRPCError , OSError , asyncio .TimeoutError ):
136161 logger .warning ("Cache connectivity check failed" )
137162
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 (
163+ podName = request .volume_context .get ("csi.storage.k8s.io/pod.name" )
164+ podNamespace = request .volume_context .get (
165+ "csi.storage.k8s.io/pod.namespace"
166+ )
167+ podUid = request .volume_context .get ("csi.storage.k8s.io/pod.uid" )
168+
169+ packagePaths = []
170+ if podName and podNamespace and podUid :
171+ pod = await Pod .get (podName , podNamespace )
172+ if pod .metadata .uid != podUid :
173+ raise GRPCError (Status .INTERNAL , "poduid doesn't match" )
174+
175+ for packagePath in set (extract_store_paths (pod .raw )):
176+ async with self .volumeLocks [storePath ]:
177+ name = extract_store_name (packagePath )
178+ result = await try_console (
149179 "nix" ,
150180 "build" ,
151181 * extraArgs ,
152182 "--print-out-paths" ,
153183 "--out-link" ,
154- gcPath ,
184+ gcRoot / name ,
155185 packagePath ,
156186 timeout = NIX_BUILD_TIMEOUT ,
157187 )
158- if flakeRef is not None and not packagePath .exists ():
188+ packagePaths .append (Path (result .stdout .splitlines ()[0 ]))
189+
190+ # Source selection order (intentional, documented in README):
191+ # 1. storePath - if present, use directly
192+ # 2. flakeRef - if storePath not present, build flake
193+ # 3. nixExpr - if neither above present, evaluate expression
194+ # Users can specify multiple; first non-None in priority order is used.
195+ if storePath is not None :
196+ async with self .volumeLocks [storePath ]:
197+ logger .debug (f"{ storePath = } " )
198+ name = extract_store_name (storePath )
199+ result = await try_console (
200+ "nix" ,
201+ "build" ,
202+ * extraArgs ,
203+ "--print-out-paths" ,
204+ "--out-link" ,
205+ gcRoot / storePath ,
206+ storePath ,
207+ timeout = NIX_BUILD_TIMEOUT ,
208+ )
209+ packagePaths .append (Path (result .stdout .splitlines ()[0 ]))
210+ if not primaryPackagePath .exists ():
211+ primaryPackagePath = Path (result .stdout .splitlines ()[0 ])
212+
213+ if flakeRef is not None :
159214 async with self .volumeLocks [flakeRef ]:
160215 logger .debug (f"{ flakeRef = } " )
161216
@@ -166,12 +221,15 @@ async def NodePublishVolume(self, stream):
166221 * extraArgs ,
167222 "--print-out-paths" ,
168223 "--out-link" ,
169- gcPath ,
224+ gcRoot / "flake" ,
170225 flakeRef ,
171226 timeout = NIX_BUILD_TIMEOUT ,
172227 )
173- packagePath = Path (result .stdout .splitlines ()[0 ])
174- if nixExpr is not None and not packagePath .exists ():
228+ packagePaths .append (Path (result .stdout .splitlines ()[0 ]))
229+ if not primaryPackagePath .exists ():
230+ primaryPackagePath = Path (result .stdout .splitlines ()[0 ])
231+
232+ if nixExpr is not None :
175233 async with self .volumeLocks [nixExpr ]:
176234 logger .debug (f"{ nixExpr = } " )
177235 with tempfile .NamedTemporaryFile (mode = "w" , suffix = ".nix" ) as tmp :
@@ -185,14 +243,16 @@ async def NodePublishVolume(self, stream):
185243 * extraArgs ,
186244 "--print-out-paths" ,
187245 "--out-link" ,
188- gcPath ,
246+ gcRoot / "expr" ,
189247 "--file" ,
190248 tmp .name ,
191249 timeout = NIX_BUILD_TIMEOUT ,
192250 )
193- packagePath = Path (result .stdout .splitlines ()[0 ])
251+ packagePaths .append (Path (result .stdout .splitlines ()[0 ]))
252+ if not primaryPackagePath .exists ():
253+ primaryPackagePath = Path (result .stdout .splitlines ()[0 ])
194254
195- if not packagePath .exists ():
255+ if not primaryPackagePath .exists ():
196256 logger .error ("packagePath doesn't exist after building" )
197257 raise GRPCError (
198258 Status .INVALID_ARGUMENT ,
@@ -207,13 +267,13 @@ async def NodePublishVolume(self, stream):
207267 # Create NIX_STATE_DIR where database will be initialized
208268 NIX_STATE_DIR .mkdir (parents = True , exist_ok = True )
209269
210- # Get closure
211- paths = (
270+ # Get storepaths from all packages
271+ storePaths = (
212272 await try_captured (
213273 "nix" ,
214274 "path-info" ,
215275 "--recursive" ,
216- packagePath ,
276+ * packagePaths ,
217277 )
218278 ).stdout .splitlines ()
219279
@@ -222,14 +282,16 @@ async def NodePublishVolume(self, stream):
222282 # extra steps. (Hardlinking instead of dumbcopying)
223283
224284 # Install CSI gcroots
225- await try_captured (
226- "nix" ,
227- "build" ,
228- "--out-link" ,
229- gcPath ,
230- packagePath ,
231- timeout = NIX_BUILD_TIMEOUT ,
232- )
285+ for packagePath in packagePaths :
286+ name = extract_store_name (packagePath )
287+ await try_captured (
288+ "nix" ,
289+ "build" ,
290+ "--out-link" ,
291+ gcRoot / name ,
292+ packagePath ,
293+ timeout = NIX_BUILD_TIMEOUT ,
294+ )
233295
234296 # Copy closure to substore, rsync saves a lot of implementation
235297 # headache here. --archive keeps all attributes, --hard-links
@@ -242,7 +304,7 @@ async def NodePublishVolume(self, stream):
242304 "--links" ,
243305 "--hard-links" ,
244306 "--mkpath" ,
245- * paths ,
307+ * storePaths ,
246308 volumeRoot / "nix/store" ,
247309 )
248310
@@ -251,21 +313,23 @@ async def NodePublishVolume(self, stream):
251313 await try_captured (
252314 "nix_init_db" ,
253315 NIX_STATE_DIR ,
254- * paths ,
316+ * storePaths ,
255317 )
256318
257- # install gcroots in container using chroot store this is
319+ # install gcroots in container using chroot, store this is
258320 # required because the auto roots created for /nix/var/result
259321 # 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- )
322+ for packagePath in packagePaths :
323+ name = extract_store_name (packagePath )
324+ await try_captured (
325+ "nix" ,
326+ "build" ,
327+ "--store" ,
328+ volumeRoot ,
329+ "--out-link" ,
330+ NIX_STATE_DIR / f"gcroots/result/{ name } " ,
331+ packagePath ,
332+ )
269333
270334 # install /nix/var/result in container using chroot store
271335 await try_captured (
@@ -275,12 +339,12 @@ async def NodePublishVolume(self, stream):
275339 volumeRoot ,
276340 "--out-link" ,
277341 volumeRoot / "nix/var/result" ,
278- packagePath ,
342+ primaryPackagePath ,
279343 )
280344 except Exception :
281345 logger .exception ("Failed to build volume" )
282346 # Remove gcroots if we failed something else
283- gcPath . unlink ( missing_ok = True )
347+ shutil . rmtree ( gcRoot , ignore_errors = True )
284348 # Remove what we were working on
285349 shutil .rmtree (volumeRoot , True )
286350 raise
@@ -297,7 +361,7 @@ async def NodePublishVolume(self, stream):
297361 "--bind" ,
298362 "-o" ,
299363 "ro" ,
300- volumeRoot / "nix" ,
364+ volumeRoot ,
301365 targetPath ,
302366 ]
303367 else :
@@ -315,7 +379,7 @@ async def NodePublishVolume(self, stream):
315379 "overlay" ,
316380 "overlay" ,
317381 "-o" ,
318- f"rw,lowerdir={ volumeRoot / 'nix' } ,upperdir={ upperdir } ,workdir={ workdir } " ,
382+ f"rw,lowerdir={ volumeRoot } ,upperdir={ upperdir } ,workdir={ workdir } " ,
319383 targetPath ,
320384 ]
321385
@@ -324,7 +388,7 @@ async def NodePublishVolume(self, stream):
324388 logger .debug (f"Mount target { targetPath } was already mounted" )
325389 elif mount .returncode != 0 :
326390 # Clean up resources on mount failure
327- gcPath . unlink ( missing_ok = True )
391+ shutil . rmtree ( gcRoot , ignore_errors = True )
328392 shutil .rmtree (volumeRoot , ignore_errors = True )
329393 raise GRPCError (
330394 Status .INTERNAL ,
@@ -334,7 +398,7 @@ async def NodePublishVolume(self, stream):
334398 reply = csi_pb2 .NodePublishVolumeResponse ()
335399 await stream .send_message (reply )
336400
337- task = asyncio .create_task (copyToCache (packagePath ))
401+ task = asyncio .create_task (copyToCache (primaryPackagePath ))
338402 task .add_done_callback (
339403 lambda t : logger .error (f"copyToCache failed: { t .exception ()} " )
340404 if t .exception ()
@@ -385,11 +449,11 @@ async def NodeUnpublishVolume(self, stream):
385449 )
386450
387451 # Remove gcroots
388- gcPath = CSI_GCROOTS / request .volume_id
389- if gcPath .exists ():
452+ gcRoot = CSI_GCROOTS / request .volume_id
453+ if gcRoot .exists ():
390454 try :
391- gcPath . unlink ( )
392- logger .debug (f"unlinked { gcPath = } " )
455+ shutil . rmtree ( gcRoot , ignore_errors = True )
456+ logger .debug (f"unlinked { gcRoot = } " )
393457 except Exception as ex :
394458 raise GRPCError (
395459 Status .INTERNAL , f"unlinking { targetPath = } failed" , ex
0 commit comments