Skip to content

Commit 2ba3beb

Browse files
committed
search the podspec for storepaths and add them to the volume
1 parent 26bf770 commit 2ba3beb

2 files changed

Lines changed: 121 additions & 57 deletions

File tree

kubenix/options.nix

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,11 +151,11 @@ in
151151
overlays = [
152152
(import ../pkgs)
153153
(
154-
self: pkgs:
154+
final: prev:
155155
let
156156
callPackage =
157-
pp:
158-
pkgs.callPackage pp {
157+
packagePath:
158+
final.callPackage packagePath {
159159
inherit (cfg) dinix;
160160
};
161161
in

python/nix_csi/service.py

Lines changed: 118 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import kr8s
33
import logging
44
import os
5+
import re
56
import shutil
67
import socket
78
import tempfile
@@ -16,7 +17,9 @@
1617
from grpclib.const import Status
1718
from grpclib.server import Server
1819
from importlib import metadata
20+
from kr8s.asyncio.objects import Pod
1921
from pathlib import Path
22+
from typing import Any, Iterator
2023

2124
logger = logging.getLogger("nix-csi")
2225

@@ -48,6 +51,27 @@
4851
NAMESPACE = os.environ.get("KUBE_NAMESPACE", "nix-csi")
4952
BUILDERS_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+
5175
async 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+
83108
class 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

Comments
 (0)