From edb1e865b89eaada5973c6fcaf1015393ff4da91 Mon Sep 17 00:00:00 2001 From: dmadisetti Date: Wed, 14 Jan 2026 13:56:51 -0800 Subject: [PATCH 1/3] feat: add [toolmarimo.env.venv] to specify a specific venv --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 17f6d0b..37ac946 100644 --- a/.gitignore +++ b/.gitignore @@ -26,8 +26,8 @@ go.work *.swo *~ -__marimo__/* -__pycache__/* +*/__marimo__/* +*/__pycache__/* *.pyc # Kubeconfig might contain secrets From f1ded3d8e71e4895294ce5ee2f03458ff4c05410 Mon Sep 17 00:00:00 2001 From: dmadisetti Date: Mon, 30 Mar 2026 15:45:16 -0700 Subject: [PATCH 2/3] feat: add nodeSelector support and fix pod overlay merge --- pkg/resources/pod.go | 14 +++ plugin/examples/gpu-getting-started.py | 2 +- plugin/kubectl_marimo/deploy.py | 6 +- plugin/kubectl_marimo/formats/python.py | 22 +++- plugin/kubectl_marimo/k8s.py | 20 ++- plugin/kubectl_marimo/resources.py | 95 +++++++++----- plugin/pyproject.toml | 5 + plugin/tests/test_formats.py | 19 +++ plugin/tests/test_resources.py | 157 +++++++++++++++++++++--- plugin/uv.lock | 33 +++++ 10 files changed, 325 insertions(+), 48 deletions(-) diff --git a/pkg/resources/pod.go b/pkg/resources/pod.go index 24e9f41..9783704 100644 --- a/pkg/resources/pod.go +++ b/pkg/resources/pod.go @@ -369,6 +369,20 @@ func buildResourceRequirements(spec *marimov1alpha1.ResourcesSpec) corev1.Resour // applyPodOverrides merges overrides into base using strategic merge patch. func applyPodOverrides(base, overrides corev1.PodSpec) corev1.PodSpec { + // Clear empty slices to prevent them from replacing base values. + // In strategic merge, empty slice [] means "replace with empty", + // while nil means "don't touch". When podOverrides is deserialized + // from YAML/JSON, absent fields become empty slices, not nil. + if len(overrides.Containers) == 0 { + overrides.Containers = nil + } + if len(overrides.InitContainers) == 0 { + overrides.InitContainers = nil + } + if len(overrides.Volumes) == 0 { + overrides.Volumes = nil + } + baseJSON, err := json.Marshal(base) if err != nil { return base diff --git a/plugin/examples/gpu-getting-started.py b/plugin/examples/gpu-getting-started.py index c9ae571..6541a9b 100644 --- a/plugin/examples/gpu-getting-started.py +++ b/plugin/examples/gpu-getting-started.py @@ -10,7 +10,7 @@ import marimo -__generated_with = "0.18.4" +__generated_with = "0.19.2" app = marimo.App() diff --git a/plugin/kubectl_marimo/deploy.py b/plugin/kubectl_marimo/deploy.py index 6b1a745..1047f9a 100644 --- a/plugin/kubectl_marimo/deploy.py +++ b/plugin/kubectl_marimo/deploy.py @@ -443,7 +443,7 @@ def deploy_notebook( name = resource_name(file_path, frontmatter) # Build resource (separates local mounts from remote) - resource, rsync_mounts, sshfs_mounts = build_marimo_notebook( + resource, rsync_mounts, sshfs_mounts, warnings = build_marimo_notebook( name=name, namespace=namespace, content=content, @@ -452,6 +452,10 @@ def deploy_notebook( source=source, ) + # Show warnings about unknown fields or other issues + for warning in warnings: + click.echo(click.style(f"Warning: {warning}", fg="yellow"), err=True) + if dry_run: click.echo(to_yaml(resource)) if rsync_mounts: diff --git a/plugin/kubectl_marimo/formats/python.py b/plugin/kubectl_marimo/formats/python.py index 2c72b3b..b7a4ff3 100644 --- a/plugin/kubectl_marimo/formats/python.py +++ b/plugin/kubectl_marimo/formats/python.py @@ -31,7 +31,8 @@ def extract_pep723_metadata(content: str) -> dict[str, Any] | None: # storage = "5Gi" """ # Look for PEP 723 script block - pattern = r"# /// script\n((?:# .*\n)*?)# ///" + # Pattern allows empty comment lines (# or #\n) as well as # followed by content + pattern = r"# /// script\n((?:#(?: .*)?\n)*?)# ///" match = re.search(pattern, content) if not match: @@ -92,6 +93,25 @@ def extract_pep723_metadata(content: str) -> dict[str, Any] | None: if env: metadata["env"] = env + # Look for marimo k8s nodeSelector config + node_selector_pattern = r"# \[tool\.marimo\.k8s\.nodeSelector\]\n((?:# .*\n)*)" + node_selector_match = re.search(node_selector_pattern, content) + if node_selector_match: + node_selector = {} + for line in node_selector_match.group(1).split("\n"): + line = line.lstrip("# ").strip() + if line.startswith("["): + # Stop at next section + break + if "=" in line: + key, _, value = line.partition("=") + key = key.strip() + value = value.strip() + value = _parse_toml_value(value) + node_selector[key] = value + if node_selector: + metadata["nodeSelector"] = node_selector + return metadata if metadata else None diff --git a/plugin/kubectl_marimo/k8s.py b/plugin/kubectl_marimo/k8s.py index 3c1623c..7984069 100644 --- a/plugin/kubectl_marimo/k8s.py +++ b/plugin/kubectl_marimo/k8s.py @@ -4,6 +4,8 @@ import sys from typing import Any +import click + def apply_resource(resource: dict[str, Any], dry_run: bool = False) -> bool: """Apply a Kubernetes resource using kubectl. @@ -27,12 +29,26 @@ def apply_resource(resource: dict[str, Any], dry_run: bool = False) -> bool: text=True, ) if result.returncode != 0: - print(f"Error: {result.stderr}", file=sys.stderr) + stderr = result.stderr + # Format CRD validation errors nicely + if "is invalid:" in stderr: + click.echo( + click.style("Invalid notebook configuration:", fg="red"), + err=True, + ) + for line in stderr.split("\n"): + line = line.strip() + if "spec." in line or "metadata." in line: + click.echo(f" • {line}", err=True) + elif line and "Error from server" not in line: + click.echo(f" {line}", err=True) + else: + click.echo(click.style(f"Error: {stderr}", fg="red"), err=True) return False print(result.stdout, end="") return True except FileNotFoundError: - print("Error: kubectl not found in PATH", file=sys.stderr) + click.echo(click.style("Error: kubectl not found in PATH", fg="red"), err=True) return False diff --git a/plugin/kubectl_marimo/resources.py b/plugin/kubectl_marimo/resources.py index 2fc82a6..05f1c41 100644 --- a/plugin/kubectl_marimo/resources.py +++ b/plugin/kubectl_marimo/resources.py @@ -10,6 +10,27 @@ # Default SSH image, configurable via environment SSH_IMAGE = os.environ.get("SSH_IMAGE", "linuxserver/openssh-server:latest") +# Fields that pass through directly from frontmatter to spec (no transformation) +PASSTHROUGH_SPEC_FIELDS = { + "image", + "port", + "resources", + "sidecars", + "source", + "podOverrides", +} + +# Fields requiring special handling +SPECIAL_FIELDS = {"auth", "nodeSelector", "env", "mounts", "storage", "title"} + +# PEP723 fields that are parsed but not used for K8s (ignored silently) +PEP723_IGNORED_FIELDS = {"dependencies", "requires-python"} + +# All known frontmatter fields (for unknown field warnings) +KNOWN_FRONTMATTER_FIELDS = ( + PASSTHROUGH_SPEC_FIELDS | SPECIAL_FIELDS | PEP723_IGNORED_FIELDS +) + def parse_mount_uri(uri: str) -> tuple[str, str]: """Parse mount URI into (scheme, path). @@ -160,7 +181,9 @@ def build_marimo_notebook( frontmatter: dict[str, Any] | None = None, mode: str = "edit", source: str | None = None, -) -> tuple[dict[str, Any], list[tuple[str, str, str]], list[tuple[str, str]]]: +) -> tuple[ + dict[str, Any], list[tuple[str, str, str]], list[tuple[str, str]], list[str] +]: """Build MarimoNotebook custom resource. Args: @@ -172,11 +195,13 @@ def build_marimo_notebook( source: Data source URI (rsync://, sshfs://, cw://) Returns: - (resource, rsync_mounts, sshfs_mounts) + (resource, rsync_mounts, sshfs_mounts, warnings) - resource: CRD dict to apply to cluster - rsync_mounts: list of (source_path, mount_point, scheme) for kubectl cp - sshfs_mounts: list of (remote_path, local_mount) for local sshfs + - warnings: list of warning messages (e.g., unknown frontmatter fields) """ + warnings: list[str] = [] spec: dict[str, Any] = { "mode": mode, } @@ -184,29 +209,42 @@ def build_marimo_notebook( # Content (file-based deployments, empty string for directory mode) spec["content"] = content if content else "" - # Default storage (PVC by notebook name) - always create PVC - storage_size = "1Gi" - if frontmatter and "storage" in frontmatter: - storage_size = frontmatter["storage"] - spec["storage"] = {"size": storage_size} - - # Apply frontmatter settings if frontmatter: - if "image" in frontmatter: - spec["image"] = frontmatter["image"] - if "port" in frontmatter: - spec["port"] = int(frontmatter["port"]) - if "auth" in frontmatter: - if frontmatter["auth"] == "none": - spec["auth"] = {} # Empty auth block = --no-token - - # Environment variables + # Warn about unknown fields (catches typos like "imge") + for field in frontmatter: + if field not in KNOWN_FRONTMATTER_FIELDS: + warnings.append(f"Unknown frontmatter field '{field}' will be ignored") + + # Pass through valid spec fields directly (no transformation needed) + for field in PASSTHROUGH_SPEC_FIELDS: + if field in frontmatter: + spec[field] = frontmatter[field] + + # Storage: handle both string shorthand and full object + storage = frontmatter.get("storage", "1Gi") + if isinstance(storage, str): + spec["storage"] = {"size": storage} + else: + spec["storage"] = storage + + # Auth: "none" → empty dict for --no-token + if frontmatter.get("auth") == "none": + spec["auth"] = {} + elif "auth" in frontmatter: + spec["auth"] = frontmatter["auth"] + + # nodeSelector → nested in podOverrides (merge with existing if present) + if "nodeSelector" in frontmatter: + spec.setdefault("podOverrides", {})["nodeSelector"] = frontmatter[ + "nodeSelector" + ] + + # Env: parse simplified syntax to K8s EnvVar format if "env" in frontmatter: spec["env"] = parse_env(frontmatter["env"]) - - # Resources (CPU, memory, GPU) - if "resources" in frontmatter: - spec["resources"] = frontmatter["resources"] + else: + # Default storage when no frontmatter + spec["storage"] = {"size": "1Gi"} # Collect mounts from --source and frontmatter all_mounts = [] @@ -223,12 +261,13 @@ def build_marimo_notebook( if cw_mounts: spec["mounts"] = cw_mounts - # Add SSH sidecars for sshfs mounts - sidecars = [] + # Add SSH sidecars for sshfs mounts (merge with existing sidecars if present) + ssh_sidecars = [] for i, _ in enumerate(sshfs_mounts): - sidecars.append(build_ssh_sidecar(i)) - if sidecars: - spec["sidecars"] = sidecars + ssh_sidecars.append(build_ssh_sidecar(i)) + if ssh_sidecars: + existing_sidecars = spec.get("sidecars", []) + spec["sidecars"] = existing_sidecars + ssh_sidecars metadata = {"name": name} if namespace is not None: @@ -240,7 +279,7 @@ def build_marimo_notebook( "metadata": metadata, "spec": spec, } - return resource, rsync_mounts, sshfs_mounts + return resource, rsync_mounts, sshfs_mounts, warnings def to_yaml(resource: dict[str, Any]) -> str: diff --git a/plugin/pyproject.toml b/plugin/pyproject.toml index 7635a85..e0f7fb4 100644 --- a/plugin/pyproject.toml +++ b/plugin/pyproject.toml @@ -53,3 +53,8 @@ packages = ["kubectl_marimo"] [tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py"] + +[dependency-groups] +dev = [ + "ruff>=0.15.8", +] diff --git a/plugin/tests/test_formats.py b/plugin/tests/test_formats.py index 60afab0..650a445 100644 --- a/plugin/tests/test_formats.py +++ b/plugin/tests/test_formats.py @@ -177,6 +177,25 @@ def test_k8s_mounts_list(self): assert meta["storage"] == "1Gi" assert meta["mounts"] == ["sshfs://user@host:/data", "cw://bucket/prefix"] + def test_k8s_node_selector(self): + content = """# /// script +# dependencies = ["marimo"] +# /// +# [tool.marimo.k8s] +# storage = "1Gi" +# +# [tool.marimo.k8s.nodeSelector] +# compute.coreweave.com/node-pool = "gpu-node-pool" +# gpu = "true" +import marimo""" + meta = extract_pep723_metadata(content) + assert meta["storage"] == "1Gi" + assert "nodeSelector" in meta + assert ( + meta["nodeSelector"]["compute.coreweave.com/node-pool"] == "gpu-node-pool" + ) + assert meta["nodeSelector"]["gpu"] == "true" + class TestIsMaimoPython: def test_import_marimo(self): diff --git a/plugin/tests/test_resources.py b/plugin/tests/test_resources.py index c274f83..422a82b 100644 --- a/plugin/tests/test_resources.py +++ b/plugin/tests/test_resources.py @@ -64,7 +64,7 @@ def test_frontmatter_takes_precedence(self): class TestBuildMarimoNotebook: def test_basic(self): - resource, rsync_mounts, sshfs_mounts = build_marimo_notebook( + resource, rsync_mounts, sshfs_mounts, warnings = build_marimo_notebook( name="test-notebook", namespace="default", content="# test content", @@ -80,9 +80,10 @@ def test_basic(self): assert resource["spec"]["storage"]["size"] == "1Gi" assert rsync_mounts == [] assert sshfs_mounts == [] + assert warnings == [] def test_with_image(self): - resource, _, _ = build_marimo_notebook( + resource, _, _, _ = build_marimo_notebook( name="test", namespace="default", content="content", @@ -91,16 +92,16 @@ def test_with_image(self): assert resource["spec"]["image"] == "custom:latest" def test_with_port(self): - resource, _, _ = build_marimo_notebook( + resource, _, _, _ = build_marimo_notebook( name="test", namespace="default", content="content", - frontmatter={"port": "8080"}, + frontmatter={"port": 8080}, ) assert resource["spec"]["port"] == 8080 def test_with_storage(self): - resource, _, _ = build_marimo_notebook( + resource, _, _, _ = build_marimo_notebook( name="test", namespace="default", content="content", @@ -108,8 +109,19 @@ def test_with_storage(self): ) 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( + resource, _, _, _ = build_marimo_notebook( name="test", namespace="default", content="content", @@ -117,8 +129,19 @@ def test_auth_none(self): ) 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( + resource, _, _, _ = build_marimo_notebook( name="test", namespace="default", content="content", @@ -127,7 +150,7 @@ def test_mode_edit(self): assert resource["spec"]["mode"] == "edit" def test_mode_run(self): - resource, _, _ = build_marimo_notebook( + resource, _, _, _ = build_marimo_notebook( name="test", namespace="default", content="content", @@ -136,7 +159,7 @@ def test_mode_run(self): assert resource["spec"]["mode"] == "run" def test_source_adds_cw_mount(self): - resource, _, _ = build_marimo_notebook( + resource, _, _, _ = build_marimo_notebook( name="test", namespace="default", content="content", @@ -145,7 +168,7 @@ def test_source_adds_cw_mount(self): assert resource["spec"]["mounts"] == ["cw://bucket/data"] def test_frontmatter_cw_mounts(self): - resource, _, _ = build_marimo_notebook( + resource, _, _, _ = build_marimo_notebook( name="test", namespace="default", content="content", @@ -154,7 +177,7 @@ def test_frontmatter_cw_mounts(self): assert resource["spec"]["mounts"] == ["cw://bucket1", "cw://bucket2"] def test_frontmatter_env(self): - resource, _, _ = build_marimo_notebook( + resource, _, _, _ = build_marimo_notebook( name="test", namespace="default", content="content", @@ -167,7 +190,7 @@ def test_frontmatter_env(self): assert debug_var["value"] == "true" def test_content_none_for_directory(self): - resource, _, _ = build_marimo_notebook( + resource, _, _, _ = build_marimo_notebook( name="test", namespace="default", content=None, # Directory mode @@ -179,7 +202,7 @@ def test_content_none_for_directory(self): def test_rsync_mount_filtered(self): """Rsync mounts should be returned separately, not in CRD.""" - resource, rsync_mounts, _ = build_marimo_notebook( + resource, rsync_mounts, _, _ = build_marimo_notebook( name="test", namespace="default", content="content", @@ -196,7 +219,7 @@ def test_rsync_mount_filtered(self): def test_sshfs_mount_adds_sidecar(self): """SSHFS mounts should add SSH sidecar and return local mount info.""" - resource, _, sshfs_mounts = build_marimo_notebook( + resource, _, sshfs_mounts, _ = build_marimo_notebook( name="test", namespace="default", content="content", @@ -215,7 +238,7 @@ def test_sshfs_mount_adds_sidecar(self): def test_mixed_mount_schemes(self): """Mix of mount schemes should be handled correctly.""" - resource, rsync_mounts, sshfs_mounts = build_marimo_notebook( + resource, rsync_mounts, sshfs_mounts, _ = build_marimo_notebook( name="test", namespace="default", content="content", @@ -237,6 +260,110 @@ def test_mixed_mount_schemes(self): # 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): diff --git a/plugin/uv.lock b/plugin/uv.lock index 34b351e..84adedf 100644 --- a/plugin/uv.lock +++ b/plugin/uv.lock @@ -200,6 +200,11 @@ dev = [ { name = "ty" }, ] +[package.dev-dependencies] +dev = [ + { name = "ruff" }, +] + [package.metadata] requires-dist = [ { name = "click", specifier = ">=8.0" }, @@ -211,6 +216,9 @@ requires-dist = [ ] provides-extras = ["dev"] +[package.metadata.requires-dev] +dev = [{ name = "ruff", specifier = ">=0.15.8" }] + [[package]] name = "kubernetes" version = "34.1.0" @@ -435,6 +443,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, ] +[[package]] +name = "ruff" +version = "0.15.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/b0/73cf7550861e2b4824950b8b52eebdcc5adc792a00c514406556c5b80817/ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e", size = 4610921, upload-time = "2026-03-26T18:39:38.675Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/92/c445b0cd6da6e7ae51e954939cb69f97e008dbe750cfca89b8cedc081be7/ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7", size = 10527394, upload-time = "2026-03-26T18:39:41.566Z" }, + { url = "https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570", size = 10905693, upload-time = "2026-03-26T18:39:30.364Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3", size = 10323044, upload-time = "2026-03-26T18:39:33.37Z" }, + { url = "https://files.pythonhosted.org/packages/67/18/1bf38e20914a05e72ef3b9569b1d5c70a7ef26cd188d69e9ca8ef588d5bf/ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94", size = 10629135, upload-time = "2026-03-26T18:39:44.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/e9/138c150ff9af60556121623d41aba18b7b57d95ac032e177b6a53789d279/ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3", size = 10348041, upload-time = "2026-03-26T18:39:52.178Z" }, + { url = "https://files.pythonhosted.org/packages/02/f1/5bfb9298d9c323f842c5ddeb85f1f10ef51516ac7a34ba446c9347d898df/ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762", size = 11121987, upload-time = "2026-03-26T18:39:55.195Z" }, + { url = "https://files.pythonhosted.org/packages/10/11/6da2e538704e753c04e8d86b1fc55712fdbdcc266af1a1ece7a51fff0d10/ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a", size = 11951057, upload-time = "2026-03-26T18:39:19.18Z" }, + { url = "https://files.pythonhosted.org/packages/83/f0/c9208c5fd5101bf87002fed774ff25a96eea313d305f1e5d5744698dc314/ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8", size = 11464613, upload-time = "2026-03-26T18:40:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1", size = 11257557, upload-time = "2026-03-26T18:39:57.972Z" }, + { url = "https://files.pythonhosted.org/packages/71/8c/382a9620038cf6906446b23ce8632ab8c0811b8f9d3e764f58bedd0c9a6f/ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec", size = 11169440, upload-time = "2026-03-26T18:39:22.205Z" }, + { url = "https://files.pythonhosted.org/packages/4d/0d/0994c802a7eaaf99380085e4e40c845f8e32a562e20a38ec06174b52ef24/ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6", size = 10605963, upload-time = "2026-03-26T18:39:46.682Z" }, + { url = "https://files.pythonhosted.org/packages/19/aa/d624b86f5b0aad7cef6bbf9cd47a6a02dfdc4f72c92a337d724e39c9d14b/ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb", size = 10357484, upload-time = "2026-03-26T18:39:49.176Z" }, + { url = "https://files.pythonhosted.org/packages/35/c3/e0b7835d23001f7d999f3895c6b569927c4d39912286897f625736e1fd04/ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8", size = 10830426, upload-time = "2026-03-26T18:40:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/f0/51/ab20b322f637b369383adc341d761eaaa0f0203d6b9a7421cd6e783d81b9/ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49", size = 11345125, upload-time = "2026-03-26T18:39:27.799Z" }, + { url = "https://files.pythonhosted.org/packages/37/e6/90b2b33419f59d0f2c4c8a48a4b74b460709a557e8e0064cf33ad894f983/ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34", size = 10571959, upload-time = "2026-03-26T18:39:36.117Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89", size = 11743893, upload-time = "2026-03-26T18:39:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" }, +] + [[package]] name = "six" version = "1.17.0" From 049398aa7b75c2398f34c2975308c4817f2b6848 Mon Sep 17 00:00:00 2001 From: dmadisetti Date: Mon, 30 Mar 2026 15:57:11 -0700 Subject: [PATCH 3/3] tidy: remove fix addressed by #7 --- pkg/resources/pod.go | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/pkg/resources/pod.go b/pkg/resources/pod.go index a4a2a18..3e0bc76 100644 --- a/pkg/resources/pod.go +++ b/pkg/resources/pod.go @@ -370,19 +370,6 @@ func buildResourceRequirements(spec *marimov1alpha1.ResourcesSpec) corev1.Resour // applyPodOverrides merges overrides into base using strategic merge patch. func applyPodOverrides(base, overrides corev1.PodSpec) corev1.PodSpec { // Clear empty slices to prevent them from replacing base values. - // In strategic merge, empty slice [] means "replace with empty", - // while nil means "don't touch". When podOverrides is deserialized - // from YAML/JSON, absent fields become empty slices, not nil. - if len(overrides.Containers) == 0 { - overrides.Containers = nil - } - if len(overrides.InitContainers) == 0 { - overrides.InitContainers = nil - } - if len(overrides.Volumes) == 0 { - overrides.Volumes = nil - } - baseJSON, err := json.Marshal(base) if err != nil { return base