-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathtest_resources.py
More file actions
508 lines (445 loc) · 18 KB
/
Copy pathtest_resources.py
File metadata and controls
508 lines (445 loc) · 18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
"""Tests for resources module."""
import pytest
from kubectl_marimo.resources import (
build_marimo_notebook,
build_ssh_sidecar,
compute_hash,
detect_content_type,
filter_mounts,
parse_env,
parse_mount_uri,
resource_name,
slugify,
)
class TestComputeHash:
def test_consistent(self):
content = "hello world"
h1 = compute_hash(content)
h2 = compute_hash(content)
assert h1 == h2
def test_prefix(self):
h = compute_hash("test")
assert h.startswith("sha256:")
def test_different_content(self):
h1 = compute_hash("content1")
h2 = compute_hash("content2")
assert h1 != h2
class TestSlugify:
def test_lowercase(self):
assert slugify("MyNotebook") == "mynotebook"
def test_special_chars(self):
assert slugify("my notebook!@#") == "my-notebook"
def test_strip_dashes(self):
assert slugify("--my-notebook--") == "my-notebook"
def test_max_length(self):
long_name = "a" * 100
result = slugify(long_name)
assert len(result) <= 63
class TestResourceName:
def test_from_file_path(self):
name = resource_name("/path/to/notebook.py")
assert name == "notebook"
def test_from_frontmatter_title(self):
name = resource_name("/path/to/file.py", {"title": "My Notebook"})
assert name == "my-notebook"
def test_frontmatter_takes_precedence(self):
name = resource_name("/path/to/other.py", {"title": "Preferred Name"})
assert name == "preferred-name"
class TestBuildMarimoNotebook:
def test_basic(self):
resource, rsync_mounts, sshfs_mounts, warnings = build_marimo_notebook(
name="test-notebook",
namespace="default",
content="# test content",
)
assert resource["apiVersion"] == "marimo.io/v1alpha1"
assert resource["kind"] == "MarimoNotebook"
assert resource["metadata"]["name"] == "test-notebook"
assert resource["metadata"]["namespace"] == "default"
assert resource["spec"]["content"] == "# test content"
# Default mode should be "edit"
assert resource["spec"]["mode"] == "edit"
# Default storage should be 1Gi
assert resource["spec"]["storage"]["size"] == "1Gi"
assert rsync_mounts == []
assert sshfs_mounts == []
assert warnings == []
def test_with_image(self):
resource, _, _, _ = build_marimo_notebook(
name="test",
namespace="default",
content="content",
frontmatter={"image": "custom:latest"},
)
assert resource["spec"]["image"] == "custom:latest"
def test_with_port(self):
resource, _, _, _ = build_marimo_notebook(
name="test",
namespace="default",
content="content",
frontmatter={"port": 8080},
)
assert resource["spec"]["port"] == 8080
def test_with_storage(self):
resource, _, _, _ = build_marimo_notebook(
name="test",
namespace="default",
content="content",
frontmatter={"storage": "5Gi"},
)
assert resource["spec"]["storage"]["size"] == "5Gi"
def test_with_storage_object(self):
"""Storage can be passed as full object with storageClassName."""
resource, _, _, _ = build_marimo_notebook(
name="test",
namespace="default",
content="content",
frontmatter={"storage": {"size": "10Gi", "storageClassName": "fast-ssd"}},
)
assert resource["spec"]["storage"]["size"] == "10Gi"
assert resource["spec"]["storage"]["storageClassName"] == "fast-ssd"
def test_auth_none(self):
resource, _, _, _ = build_marimo_notebook(
name="test",
namespace="default",
content="content",
frontmatter={"auth": "none"},
)
assert resource["spec"]["auth"] == {}
def test_auth_object_passthrough(self):
"""Auth object should pass through when not 'none'."""
auth_config = {"password": {"secretKeyRef": {"name": "my-secret", "key": "pw"}}}
resource, _, _, _ = build_marimo_notebook(
name="test",
namespace="default",
content="content",
frontmatter={"auth": auth_config},
)
assert resource["spec"]["auth"] == auth_config
def test_mode_edit(self):
resource, _, _, _ = build_marimo_notebook(
name="test",
namespace="default",
content="content",
mode="edit",
)
assert resource["spec"]["mode"] == "edit"
def test_mode_run(self):
resource, _, _, _ = build_marimo_notebook(
name="test",
namespace="default",
content="content",
mode="run",
)
assert resource["spec"]["mode"] == "run"
def test_source_adds_cw_mount(self):
resource, _, _, _ = build_marimo_notebook(
name="test",
namespace="default",
content="content",
source="cw://bucket/data",
)
assert resource["spec"]["mounts"] == ["cw://bucket/data"]
def test_frontmatter_cw_mounts(self):
resource, _, _, _ = build_marimo_notebook(
name="test",
namespace="default",
content="content",
frontmatter={"mounts": ["cw://bucket1", "cw://bucket2"]},
)
assert resource["spec"]["mounts"] == ["cw://bucket1", "cw://bucket2"]
def test_frontmatter_env(self):
resource, _, _, _ = build_marimo_notebook(
name="test",
namespace="default",
content="content",
frontmatter={"env": {"DEBUG": "true", "LOG_LEVEL": "info"}},
)
env_vars = resource["spec"]["env"]
assert len(env_vars) == 2
# Check inline values
debug_var = next(e for e in env_vars if e["name"] == "DEBUG")
assert debug_var["value"] == "true"
def test_content_none_for_directory(self):
resource, _, _, _ = build_marimo_notebook(
name="test",
namespace="default",
content=None, # Directory mode
)
# Empty content for directory mode (satisfies operator validation)
assert resource["spec"]["content"] == ""
assert resource["spec"]["mode"] == "edit"
assert resource["spec"]["storage"]["size"] == "1Gi"
def test_rsync_mount_filtered(self):
"""Rsync mounts should be returned separately, not in CRD."""
resource, rsync_mounts, _, _ = build_marimo_notebook(
name="test",
namespace="default",
content="content",
source="rsync://./local/data:/mnt/data",
)
# Rsync mounts should NOT be in CRD
assert "mounts" not in resource["spec"]
# Local mount should be returned separately
assert len(rsync_mounts) == 1
src, dest, scheme = rsync_mounts[0]
assert src == "./local/data"
assert dest == "/mnt/data"
assert scheme == "rsync"
def test_sshfs_mount_adds_sidecar(self):
"""SSHFS mounts should add SSH sidecar and return local mount info."""
resource, _, sshfs_mounts, _ = build_marimo_notebook(
name="test",
namespace="default",
content="content",
source="sshfs:///home/marimo/notebooks",
)
# Should have sidecar added
assert "sidecars" in resource["spec"]
assert len(resource["spec"]["sidecars"]) == 1
sidecar = resource["spec"]["sidecars"][0]
assert sidecar["name"] == "sshfs-0"
assert sidecar["exposePort"] == 2222
# Should return sshfs mount info
assert len(sshfs_mounts) == 1
remote_path, local_mount = sshfs_mounts[0]
assert remote_path == "/home/marimo/notebooks"
def test_mixed_mount_schemes(self):
"""Mix of mount schemes should be handled correctly."""
resource, rsync_mounts, sshfs_mounts, _ = build_marimo_notebook(
name="test",
namespace="default",
content="content",
frontmatter={
"mounts": [
"rsync://./local/path", # Rsync - plugin handles
"sshfs:///data", # SSHFS - plugin handles
"cw://bucket/path", # CW - operator handles
]
},
)
# Only CW mount should be in CRD mounts
assert resource["spec"]["mounts"] == ["cw://bucket/path"]
# Should have sshfs sidecar
assert "sidecars" in resource["spec"]
assert len(resource["spec"]["sidecars"]) == 1
# Rsync should be separate
assert len(rsync_mounts) == 1
# SSHFS should be separate
assert len(sshfs_mounts) == 1
def test_node_selector(self):
"""nodeSelector should map to podOverrides."""
resource, _, _, _ = build_marimo_notebook(
name="test",
namespace="default",
content="content",
frontmatter={
"nodeSelector": {
"compute.coreweave.com/node-pool": "gpu-node-pool",
"gpu": "true",
}
},
)
assert "podOverrides" in resource["spec"]
assert "nodeSelector" in resource["spec"]["podOverrides"]
node_selector = resource["spec"]["podOverrides"]["nodeSelector"]
assert node_selector["compute.coreweave.com/node-pool"] == "gpu-node-pool"
assert node_selector["gpu"] == "true"
def test_unknown_field_warning(self):
"""Unknown frontmatter fields should produce warnings."""
resource, _, _, warnings = build_marimo_notebook(
name="test",
namespace="default",
content="content",
frontmatter={"imge": "typo:latest", "unknownField": "value"},
)
assert len(warnings) == 2
assert any("imge" in w for w in warnings)
assert any("unknownField" in w for w in warnings)
# Typo field should NOT be in spec
assert "imge" not in resource["spec"]
def test_passthrough_resources(self):
"""Resources should pass through directly."""
resources_config = {
"requests": {"cpu": "1", "memory": "2Gi"},
"limits": {"cpu": "2", "memory": "4Gi", "nvidia.com/gpu": "1"},
}
resource, _, _, _ = build_marimo_notebook(
name="test",
namespace="default",
content="content",
frontmatter={"resources": resources_config},
)
assert resource["spec"]["resources"] == resources_config
def test_passthrough_sidecars(self):
"""Sidecars should pass through directly."""
sidecars_config = [
{"name": "helper", "image": "busybox:latest"},
]
resource, _, _, _ = build_marimo_notebook(
name="test",
namespace="default",
content="content",
frontmatter={"sidecars": sidecars_config},
)
assert resource["spec"]["sidecars"] == sidecars_config
def test_sidecars_merge_with_sshfs(self):
"""User sidecars should merge with auto-generated SSH sidecars."""
resource, _, sshfs_mounts, _ = build_marimo_notebook(
name="test",
namespace="default",
content="content",
frontmatter={"sidecars": [{"name": "helper", "image": "busybox"}]},
source="sshfs:///data",
)
# Should have both user sidecar and SSH sidecar
assert len(resource["spec"]["sidecars"]) == 2
names = [s["name"] for s in resource["spec"]["sidecars"]]
assert "helper" in names
assert "sshfs-0" in names
def test_passthrough_podOverrides(self):
"""podOverrides should pass through directly."""
pod_overrides = {
"nodeSelector": {"zone": "us-east"},
"tolerations": [{"key": "gpu", "effect": "NoSchedule"}],
}
resource, _, _, _ = build_marimo_notebook(
name="test",
namespace="default",
content="content",
frontmatter={"podOverrides": pod_overrides},
)
assert resource["spec"]["podOverrides"] == pod_overrides
def test_nodeSelector_merges_with_podOverrides(self):
"""nodeSelector should merge into existing podOverrides."""
resource, _, _, _ = build_marimo_notebook(
name="test",
namespace="default",
content="content",
frontmatter={
"podOverrides": {"tolerations": [{"key": "gpu"}]},
"nodeSelector": {"zone": "us-east"},
},
)
# Both should be present
assert resource["spec"]["podOverrides"]["tolerations"] == [{"key": "gpu"}]
assert resource["spec"]["podOverrides"]["nodeSelector"] == {"zone": "us-east"}
class TestParseEnv:
def test_inline_value(self):
result = parse_env({"DEBUG": "true"})
assert result == [{"name": "DEBUG", "value": "true"}]
def test_secret_reference(self):
result = parse_env({"API_KEY": {"secret": "my-secret", "key": "api-key"}})
assert len(result) == 1
assert result[0]["name"] == "API_KEY"
assert result[0]["valueFrom"]["secretKeyRef"]["name"] == "my-secret"
assert result[0]["valueFrom"]["secretKeyRef"]["key"] == "api-key"
def test_secret_default_key(self):
result = parse_env({"API_KEY": {"secret": "my-secret"}})
# Default key should be lowercase of env var name
assert result[0]["valueFrom"]["secretKeyRef"]["key"] == "api_key"
def test_mixed_env(self):
result = parse_env(
{
"DEBUG": "true",
"API_KEY": {"secret": "my-secret", "key": "key"},
}
)
assert len(result) == 2
debug_var = next(e for e in result if e["name"] == "DEBUG")
api_var = next(e for e in result if e["name"] == "API_KEY")
assert debug_var["value"] == "true"
assert "valueFrom" in api_var
class TestDetectContentType:
def test_markdown_frontmatter(self):
content = "---\ntitle: Test\n---\n# Heading"
assert detect_content_type(content) == "markdown"
def test_markdown_code_block(self):
content = "# Title\n```python {.marimo}\nprint('hi')\n```"
assert detect_content_type(content) == "markdown"
def test_python_default(self):
content = "import marimo\napp = marimo.App()"
assert detect_content_type(content) == "python"
def test_empty_is_python(self):
assert detect_content_type("") == "python"
class TestParseMountUri:
def test_sshfs_absolute(self):
"""sshfs:///path = local sshfs mount."""
scheme, path = parse_mount_uri("sshfs:///home/marimo/notebooks")
assert scheme == "sshfs"
assert path == "/home/marimo/notebooks"
def test_rsync_relative(self):
"""rsync://./path = relative local path."""
scheme, path = parse_mount_uri("rsync://./local/data")
assert scheme == "rsync"
assert path == "./local/data"
def test_rsync_with_mount(self):
"""rsync://./local/data:/mnt/data = rsync with mount point."""
scheme, path = parse_mount_uri("rsync://./local/data:/mnt/data")
assert scheme == "rsync"
assert path == "./local/data:/mnt/data"
def test_cw_bucket(self):
"""cw://bucket/path = CoreWeave S3."""
scheme, path = parse_mount_uri("cw://mybucket/data")
assert scheme == "cw"
assert path == "mybucket/data"
def test_invalid_uri(self):
with pytest.raises(ValueError):
parse_mount_uri("invalid")
class TestFilterMounts:
def test_separates_schemes(self):
"""Mounts should be categorized by scheme."""
mounts = [
"rsync://./local/path",
"sshfs:///data",
"cw://bucket/path",
]
cw_mounts, rsync_mounts, sshfs_mounts = filter_mounts(mounts)
assert cw_mounts == ["cw://bucket/path"]
assert len(rsync_mounts) == 1
assert rsync_mounts[0][0] == "./local/path" # source
assert rsync_mounts[0][2] == "rsync" # scheme
assert len(sshfs_mounts) == 1
assert sshfs_mounts[0][0] == "/data" # remote path
def test_rsync_default_mount_point(self):
mounts = ["rsync://./path1", "rsync://./path2"]
cw, rsync, sshfs = filter_mounts(mounts)
assert cw == []
assert len(rsync) == 2
# Check default mount points use index
assert rsync[0][1] == "/home/marimo/notebooks/mounts/local-0"
assert rsync[1][1] == "/home/marimo/notebooks/mounts/local-1"
def test_rsync_custom_mount_point(self):
mounts = ["rsync://./src:/dest"]
cw, rsync, sshfs = filter_mounts(mounts)
assert rsync[0][0] == "./src"
assert rsync[0][1] == "/dest"
def test_sshfs_mount_info(self):
mounts = ["sshfs:///home/marimo/notebooks"]
cw, rsync, sshfs = filter_mounts(mounts)
assert len(sshfs) == 1
remote_path, local_mount = sshfs[0]
assert remote_path == "/home/marimo/notebooks"
assert local_mount.startswith("./marimo-mount-")
def test_unknown_scheme_passes_through(self):
"""Unknown schemes should pass through to operator."""
mounts = ["nfs://server/path"]
cw, rsync, sshfs = filter_mounts(mounts)
assert cw == ["nfs://server/path"] # Unknown goes to operator
assert rsync == []
assert sshfs == []
class TestBuildSshSidecar:
def test_basic(self):
sidecar = build_ssh_sidecar(0)
assert sidecar["name"] == "sshfs-0"
assert sidecar["exposePort"] == 2222
assert any(
e["name"] == "PASSWORD_ACCESS" and e["value"] == "false"
for e in sidecar["env"]
)
assert any(
e["name"] == "USER_NAME" and e["value"] == "marimo" for e in sidecar["env"]
)
def test_index(self):
sidecar = build_ssh_sidecar(3)
assert sidecar["name"] == "sshfs-3"