Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions capd/devspace.local-dev.capd.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"description": "Nocalhost-style inner-loop over the continuum: an isolated per-user namespace (DevSpace), Signadot-style header-routed sandboxes that share a baseline, and the agent-machine as a StatefulSet with a per-replica TopoLVM inception mount. Consumes the control plane; surfaced to humans via the portal and to agents via the MCP ops surface (one governed source, two views).",
"links": {
"engine": "tools/devspace.py",
"devmode": "tools/devmode.py",
"portal": "tools/portal_server.py",
"agent_surface": "tools/mcp_ops_server.py",
"inception_storage": "deploy/topolvm/ (TopoLVM StorageClass + runbook)",
Expand Down
77 changes: 77 additions & 0 deletions tools/devmode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""Nocalhost-style dev-mode — the tight modify->result inner loop against a real cluster.

The whole point of a local-first PaaS: edit on the low-mem box, see it running in a real DevSpace
pod in seconds, without rebuilding an image. Dev-mode does the three things Nocalhost's Dev Mode
does, governed and in the user's own DevSpace namespace:

1. put a workload into DEV MODE — swap its container for a dev-runner with an in-pod workspace, so
synced code runs live (no image rebuild);
2. SYNC local files into the running pod (hot reload) — `kubectl cp`;
3. PORT-FORWARD the remote service (+ a debug port) to localhost (local access + remote debug).

The manifest patch and the kubectl command lines are pure functions (unit-tested); a driver wraps
them for a live cluster.
"""
from __future__ import annotations

DEV_WORKSPACE = "/workspace"


def devmode_patch(*, workload: str, dev_image: str = "python:3.12-alpine",
run_cmd: str | None = None, grant_id: str | None = None) -> dict:
"""A strategic-merge patch that puts a Deployment into dev mode: a dev-runner container with an
emptyDir workspace at /workspace, running the synced code (or idling until synced)."""
labels = {"sourceos.io/devmode": "on", "sourceos.io/workload": workload}
if grant_id:
labels["sourceos.io/grant-id"] = grant_id
container = {"name": "dev", "image": dev_image, "workingDir": DEV_WORKSPACE,
"command": ["sh", "-c", run_cmd or "sleep infinity"],
"volumeMounts": [{"name": "workspace", "mountPath": DEV_WORKSPACE}]}
return {"metadata": {"labels": labels},
"spec": {"template": {"metadata": {"labels": labels},
"spec": {"containers": [container],
"volumes": [{"name": "workspace", "emptyDir": {}}]}}}}


def _ctx(context):
return ["--context", context] if context else []


def sync_command(*, local_dir: str, namespace: str, pod: str, container: str = "dev",
remote: str = DEV_WORKSPACE, context: str | None = None) -> list:
"""`kubectl cp` local -> pod: the file-sync / hot-reload step (no image rebuild)."""
return ["kubectl", *_ctx(context), "cp", local_dir, f"{namespace}/{pod}:{remote}", "-c", container]


def port_forward_command(*, namespace: str, pod: str, ports: list, context: str | None = None) -> list:
"""`kubectl port-forward` local:remote — local access + a debug port. ports = [(local, remote)]."""
maps = [f"{lo}:{re}" for lo, re in ports]
return ["kubectl", *_ctx(context), "port-forward", "-n", namespace, f"pod/{pod}", *maps]


def devmode_plan(*, workload: str, namespace: str, local_dir: str, ports: list,
dev_image: str = "python:3.12-alpine", run_cmd: str | None = None,
context: str | None = None, grant_id: str | None = None) -> dict:
"""The full inner-loop plan: patch the workload into dev-mode, sync files, forward ports."""
return {
"workspace": DEV_WORKSPACE,
"patch": devmode_patch(workload=workload, dev_image=dev_image, run_cmd=run_cmd, grant_id=grant_id),
"patch_command": ["kubectl", *_ctx(context), "-n", namespace, "patch", "deployment", workload,
"--type", "strategic", "-p", "<patch-json>"],
"sync": sync_command(local_dir=local_dir, namespace=namespace, pod=f"<{workload}-pod>",
context=context),
"forward": port_forward_command(namespace=namespace, pod=f"<{workload}-pod>", ports=ports,
context=context),
}


if __name__ == "__main__":
import json
plan = devmode_plan(workload="productpage", namespace="ds-acme-alice-feat",
local_dir="./app", ports=[(8080, 8080), (5678, 5678)],
run_cmd="python -m http.server 8080")
print(json.dumps({"workspace": plan["workspace"],
"dev_container": plan["patch"]["spec"]["template"]["spec"]["containers"][0]["name"],
"sync": " ".join(plan["sync"]),
"forward": " ".join(plan["forward"])}, indent=2))
41 changes: 41 additions & 0 deletions tools/test_devmode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""Tests for dev-mode manifest/command generation (the pure core of the inner loop)."""
import devmode as dm


def test_devmode_patch_swaps_in_a_dev_runner_with_a_workspace():
p = dm.devmode_patch(workload="productpage", run_cmd="python -m http.server 8080", grant_id="g1")
c = p["spec"]["template"]["spec"]["containers"][0]
assert c["name"] == "dev" and c["workingDir"] == "/workspace"
assert c["command"] == ["sh", "-c", "python -m http.server 8080"]
assert c["volumeMounts"][0]["mountPath"] == "/workspace"
assert p["spec"]["template"]["spec"]["volumes"][0]["emptyDir"] == {}
assert p["metadata"]["labels"]["sourceos.io/devmode"] == "on"
assert p["metadata"]["labels"]["sourceos.io/grant-id"] == "g1"


def test_sync_command_is_kubectl_cp_into_the_workspace():
cmd = dm.sync_command(local_dir="./app", namespace="ds-x", pod="pp-abc", context="kind-x")
assert cmd == ["kubectl", "--context", "kind-x", "cp", "./app", "ds-x/pp-abc:/workspace", "-c", "dev"]


def test_port_forward_maps_local_to_remote():
cmd = dm.port_forward_command(namespace="ds-x", pod="pp-abc", ports=[(8080, 8080), (5678, 5678)])
assert cmd == ["kubectl", "port-forward", "-n", "ds-x", "pod/pp-abc", "8080:8080", "5678:5678"]


def test_plan_ties_patch_sync_and_forward():
plan = dm.devmode_plan(workload="pp", namespace="ds-x", local_dir="./app",
ports=[(8080, 8080)], run_cmd="python -m http.server 8080")
assert plan["workspace"] == "/workspace"
assert "cp" in plan["sync"] and "port-forward" in plan["forward"]
assert plan["patch"]["spec"]["template"]["spec"]["containers"][0]["name"] == "dev"


if __name__ == "__main__":
import sys
fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
for fn in fns:
fn()
print(f"ok: {len(fns)} devmode tests passed")
sys.exit(0)
1 change: 1 addition & 0 deletions tools/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"tools/devspace.py",
"tools/work_unit.py",
"tools/lease_scheduler.py",
"tools/devmode.py",
]
CAPD_KEYS = ("capability_id", "kind", "status", "links", "composes_with", "policy")
# Every CapD in capd/ must carry the core keys and parse — not just the flagship control-plane one.
Expand Down
Loading