Skip to content

Commit c689209

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

2 files changed

Lines changed: 120 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: 117 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,26 @@
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: Path | str) -> str:
71+
return str(value).removeprefix("/nix/store/")
72+
73+
5174
async 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+
83107
class 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

Comments
 (0)