Skip to content

Commit 6077311

Browse files
committed
Add support for podman as container runtime
1 parent 0c2d487 commit 6077311

13 files changed

Lines changed: 382 additions & 57 deletions

File tree

src/vibepod/commands/list_cmd.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99

1010
from vibepod.constants import DEFAULT_IMAGES, EXIT_DOCKER_NOT_RUNNING, SUPPORTED_AGENTS
1111
from vibepod.core.agents import get_agent_shortcut
12-
from vibepod.core.docker import DockerClientError, DockerManager
12+
from vibepod.core.config import get_config
13+
from vibepod.core.docker import DockerClientError, get_manager
1314
from vibepod.utils.console import console, error
1415

1516

@@ -49,10 +50,14 @@ def list_agents(
4950
bool, typer.Option("-r", "--running", help="Show only running agents")
5051
] = False,
5152
as_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
53+
runtime: Annotated[
54+
str | None,
55+
typer.Option("--runtime", help="Container runtime to use (docker or podman)"),
56+
] = None,
5257
) -> None:
5358
"""List available agents and running containers."""
5459
try:
55-
manager = DockerManager()
60+
manager = get_manager(runtime_override=runtime, config=get_config())
5661
containers = manager.list_managed(all_containers=True)
5762
except DockerClientError as exc:
5863
if running:

src/vibepod/commands/logs.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from vibepod.constants import EXIT_DOCKER_NOT_RUNNING
1212
from vibepod.core.config import get_config
13-
from vibepod.core.docker import DockerClientError, DockerManager
13+
from vibepod.core.docker import DockerClientError, get_manager
1414
from vibepod.utils.console import error, info, success, warning
1515

1616
app = typer.Typer(help="View logs and traffic UI")
@@ -20,6 +20,10 @@
2020
def logs_start(
2121
port: Annotated[int | None, typer.Option("--port", help="Datasette host port")] = None,
2222
no_open: Annotated[bool, typer.Option("--no-open", help="Do not open browser")] = False,
23+
runtime: Annotated[
24+
str | None,
25+
typer.Option("--runtime", help="Container runtime to use (docker or podman)"),
26+
] = None,
2327
) -> None:
2428
"""Start or reuse Datasette for session and proxy logs."""
2529
config = get_config()
@@ -34,7 +38,7 @@ def logs_start(
3438
).expanduser()
3539

3640
try:
37-
manager = DockerManager()
41+
manager = get_manager(runtime_override=runtime, config=config)
3842
except DockerClientError as exc:
3943
error(str(exc))
4044
raise typer.Exit(EXIT_DOCKER_NOT_RUNNING) from exc
@@ -55,10 +59,14 @@ def logs_start(
5559
@app.command("stop")
5660
def logs_stop(
5761
force: Annotated[bool, typer.Option("-f", "--force", help="Force stop")] = False,
62+
runtime: Annotated[
63+
str | None,
64+
typer.Option("--runtime", help="Container runtime to use (docker or podman)"),
65+
] = None,
5866
) -> None:
5967
"""Stop the Datasette container."""
6068
try:
61-
manager = DockerManager()
69+
manager = get_manager(runtime_override=runtime, config=get_config())
6270
except DockerClientError as exc:
6371
error(str(exc))
6472
raise typer.Exit(EXIT_DOCKER_NOT_RUNNING) from exc
@@ -73,10 +81,15 @@ def logs_stop(
7381

7482

7583
@app.command("status")
76-
def logs_status() -> None:
84+
def logs_status(
85+
runtime: Annotated[
86+
str | None,
87+
typer.Option("--runtime", help="Container runtime to use (docker or podman)"),
88+
] = None,
89+
) -> None:
7790
"""Show Datasette container status."""
7891
try:
79-
manager = DockerManager()
92+
manager = get_manager(runtime_override=runtime, config=get_config())
8093
except DockerClientError as exc:
8194
error(str(exc))
8295
raise typer.Exit(EXIT_DOCKER_NOT_RUNNING) from exc
@@ -94,6 +107,10 @@ def logs_status() -> None:
94107
def logs_ui(
95108
port: Annotated[int | None, typer.Option("--port", help="Datasette host port")] = None,
96109
no_open: Annotated[bool, typer.Option("--no-open", help="Do not open browser")] = False,
110+
runtime: Annotated[
111+
str | None,
112+
typer.Option("--runtime", help="Container runtime to use (docker or podman)"),
113+
] = None,
97114
) -> None:
98115
"""Alias for `vp logs start`."""
99-
logs_start(port=port, no_open=no_open)
116+
logs_start(port=port, no_open=no_open, runtime=runtime)

src/vibepod/commands/proxy.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,19 @@
99

1010
from vibepod.constants import EXIT_DOCKER_NOT_RUNNING
1111
from vibepod.core.config import get_config
12-
from vibepod.core.docker import DockerClientError, DockerManager
12+
from vibepod.core.docker import DockerClientError, get_manager
1313
from vibepod.utils.console import error, info, success, warning
1414

1515
app = typer.Typer(help="Manage the HTTP(S) proxy")
1616

1717

1818
@app.command("start")
19-
def proxy_start() -> None:
19+
def proxy_start(
20+
runtime: Annotated[
21+
str | None,
22+
typer.Option("--runtime", help="Container runtime to use (docker or podman)"),
23+
] = None,
24+
) -> None:
2025
"""Start the proxy container."""
2126
config = get_config()
2227
proxy_cfg = config.get("proxy", {})
@@ -35,7 +40,7 @@ def proxy_start() -> None:
3540
network_name = str(config.get("network", "vibepod-network"))
3641

3742
try:
38-
manager = DockerManager()
43+
manager = get_manager(runtime_override=runtime, config=config)
3944
except DockerClientError as exc:
4045
error(str(exc))
4146
raise typer.Exit(EXIT_DOCKER_NOT_RUNNING) from exc
@@ -55,10 +60,14 @@ def proxy_start() -> None:
5560
@app.command("stop")
5661
def proxy_stop(
5762
force: Annotated[bool, typer.Option("-f", "--force", help="Force stop")] = False,
63+
runtime: Annotated[
64+
str | None,
65+
typer.Option("--runtime", help="Container runtime to use (docker or podman)"),
66+
] = None,
5867
) -> None:
5968
"""Stop the proxy container."""
6069
try:
61-
manager = DockerManager()
70+
manager = get_manager(runtime_override=runtime, config=get_config())
6271
except DockerClientError as exc:
6372
error(str(exc))
6473
raise typer.Exit(EXIT_DOCKER_NOT_RUNNING) from exc
@@ -73,10 +82,15 @@ def proxy_stop(
7382

7483

7584
@app.command("status")
76-
def proxy_status() -> None:
85+
def proxy_status(
86+
runtime: Annotated[
87+
str | None,
88+
typer.Option("--runtime", help="Container runtime to use (docker or podman)"),
89+
] = None,
90+
) -> None:
7791
"""Show proxy container status."""
7892
try:
79-
manager = DockerManager()
93+
manager = get_manager(runtime_override=runtime, config=get_config())
8094
except DockerClientError as exc:
8195
error(str(exc))
8296
raise typer.Exit(EXIT_DOCKER_NOT_RUNNING) from exc

src/vibepod/commands/run.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from rich.prompt import Confirm, Prompt
1515

1616
from vibepod import __version__
17-
from vibepod.constants import EXIT_DOCKER_NOT_RUNNING, SUPPORTED_AGENTS
17+
from vibepod.constants import EXIT_DOCKER_NOT_RUNNING, RUNTIME_PODMAN, SUPPORTED_AGENTS
1818
from vibepod.core.agents import (
1919
agent_config_dir,
2020
effective_agent_image,
@@ -23,7 +23,7 @@
2323
resolve_agent_name,
2424
)
2525
from vibepod.core.config import get_config
26-
from vibepod.core.docker import DockerClientError, DockerManager
26+
from vibepod.core.docker import DockerClientError, DockerManager, get_manager
2727
from vibepod.core.session_logger import SessionLogger
2828
from vibepod.utils.console import error, info, success, warning
2929

@@ -214,6 +214,10 @@ def run(
214214
str | None,
215215
typer.Option("--network", help="Additional Docker network to connect the container to"),
216216
] = None,
217+
runtime: Annotated[
218+
str | None,
219+
typer.Option("--runtime", help="Container runtime to use (docker or podman)"),
220+
] = None,
217221
) -> None:
218222
"""Start an agent container."""
219223
config = get_config()
@@ -247,7 +251,7 @@ def run(
247251
image = effective_agent_image(selected_agent, config)
248252

249253
try:
250-
manager = DockerManager()
254+
manager = get_manager(runtime_override=runtime, config=config)
251255
except DockerClientError as exc:
252256
error(str(exc))
253257
raise typer.Exit(EXIT_DOCKER_NOT_RUNNING) from exc
@@ -266,14 +270,30 @@ def run(
266270

267271
command = spec.command
268272
entrypoint: list[str] | None = None
269-
if init_commands:
270-
info(f"Applying {len(init_commands)} init command(s) before startup")
273+
274+
# On rootless Podman the container starts as root (mapped from the host
275+
# UID) but often switches to a non-root user via su/gosu. That non-root
276+
# user gets a subordinate UID that can't read files owned by root inside
277+
# the container. Injecting a chmod before the real entrypoint ensures the
278+
# config mount is accessible after the user switch.
279+
podman_fixup: list[str] = []
280+
if manager.runtime == RUNTIME_PODMAN:
281+
paths_to_fix = [spec.config_mount_path]
282+
extra_volumes_pre = _agent_extra_volumes(selected_agent, agent_config_dir(selected_agent))
283+
if extra_volumes_pre:
284+
paths_to_fix.extend(cp for _, cp, _ in extra_volumes_pre)
285+
quoted = " ".join(f"'{p}'" for p in dict.fromkeys(paths_to_fix))
286+
podman_fixup = [f"chmod -R a+rwX {quoted} 2>/dev/null || true"]
287+
288+
if init_commands or podman_fixup:
289+
if init_commands:
290+
info(f"Applying {len(init_commands)} init command(s) before startup")
271291
try:
272292
command = manager.resolve_launch_command(image=image, command=spec.command)
273293
except DockerClientError as exc:
274294
error(str(exc))
275295
raise typer.Exit(1) from exc
276-
entrypoint = _init_entrypoint(init_commands)
296+
entrypoint = _init_entrypoint(podman_fixup + init_commands)
277297

278298
config_dir = agent_config_dir(selected_agent)
279299
config_dir.mkdir(parents=True, exist_ok=True)

src/vibepod/commands/stop.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
import typer
88

99
from vibepod.constants import EXIT_DOCKER_NOT_RUNNING
10-
from vibepod.core.docker import DockerClientError, DockerManager
10+
from vibepod.core.config import get_config
11+
from vibepod.core.docker import DockerClientError, get_manager
1112
from vibepod.utils.console import error, success
1213

1314

@@ -18,13 +19,17 @@ def stop(
1819
typer.Option("-a", "--all", help="Stop all VibePod managed containers"),
1920
] = False,
2021
force: Annotated[bool, typer.Option("-f", "--force", help="Force stop")] = False,
22+
runtime: Annotated[
23+
str | None,
24+
typer.Option("--runtime", help="Container runtime to use (docker or podman)"),
25+
] = None,
2126
) -> None:
2227
"""Stop one agent container, or all managed containers."""
2328
if not all_containers and agent is None:
2429
raise typer.BadParameter("Provide an AGENT or use --all")
2530

2631
try:
27-
manager = DockerManager()
32+
manager = get_manager(runtime_override=runtime, config=get_config())
2833
except DockerClientError as exc:
2934
error(str(exc))
3035
raise typer.Exit(EXIT_DOCKER_NOT_RUNNING) from exc

src/vibepod/commands/update.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,34 @@
88
import typer
99

1010
from vibepod import __version__
11-
from vibepod.core.docker import DockerClientError, DockerManager
11+
from vibepod.core.config import get_config
12+
from vibepod.core.docker import DockerClientError, get_manager
1213

1314

14-
def _docker_version() -> str:
15+
def _runtime_version(runtime_override: str | None = None) -> tuple[str, str]:
16+
"""Return (runtime_name, version_string)."""
1517
try:
16-
manager = DockerManager()
18+
manager = get_manager(runtime_override=runtime_override, config=get_config())
1719
version_info: dict[str, Any] = manager.client.version()
18-
return str(version_info.get("Version", "unknown"))
20+
return manager.runtime, str(version_info.get("Version", "unknown"))
1921
except DockerClientError:
20-
return "unavailable"
22+
return "unknown", "unavailable"
2123

2224

2325
def version(
2426
as_json: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False,
27+
runtime: Annotated[
28+
str | None,
29+
typer.Option("--runtime", help="Container runtime to use (docker or podman)"),
30+
] = None,
2531
) -> None:
2632
"""Show version and runtime information."""
33+
rt_name, rt_version = _runtime_version(runtime_override=runtime)
2734
info = {
2835
"vibepod": __version__,
2936
"python": platform.python_version(),
30-
"docker": _docker_version(),
37+
"runtime": rt_name,
38+
"runtime_version": rt_version,
3139
}
3240

3341
if as_json:
@@ -38,4 +46,4 @@ def version(
3846

3947
print(f"VibePod CLI: {info['vibepod']}")
4048
print(f"Python: {info['python']}")
41-
print(f"Docker: {info['docker']}")
49+
print(f"Runtime: {info['runtime']} {info['runtime_version']}")

src/vibepod/constants.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@
1818
DOCKER_NETWORK = "vibepod-network"
1919
CONTAINER_LABEL_MANAGED = "vibepod.managed"
2020

21+
# Container runtime constants
22+
RUNTIME_AUTO = "auto"
23+
RUNTIME_DOCKER = "docker"
24+
RUNTIME_PODMAN = "podman"
25+
SUPPORTED_RUNTIMES = (RUNTIME_DOCKER, RUNTIME_PODMAN)
26+
27+
DOCKER_SOCKET = "unix:///var/run/docker.sock"
28+
PODMAN_SOCKET_ROOTLESS = "unix:///run/user/{uid}/podman/podman.sock"
29+
PODMAN_SOCKET_ROOTFUL = "unix:///run/podman/podman.sock"
30+
2131
SUPPORTED_AGENTS = (
2232
"claude",
2333
"gemini",

src/vibepod/core/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
DEFAULT_ALIASES,
1414
DEFAULT_IMAGES,
1515
PROJECT_CONFIG_FILE,
16+
RUNTIME_AUTO,
1617
)
1718

1819

@@ -30,6 +31,7 @@ def _default_config() -> dict[str, Any]:
3031
"version": 1,
3132
"default_agent": "claude",
3233
"auto_pull": True,
34+
"container_runtime": RUNTIME_AUTO,
3335
"auto_remove": True,
3436
"network": "vibepod-network",
3537
"log_level": "info",
@@ -141,6 +143,7 @@ def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]
141143
def _apply_env(config: dict[str, Any]) -> dict[str, Any]:
142144
mappings: dict[str, tuple[str, Any]] = {
143145
"VP_DEFAULT_AGENT": ("default_agent", str),
146+
"VP_CONTAINER_RUNTIME": ("container_runtime", str),
144147
"VP_AUTO_PULL": ("auto_pull", lambda x: x.lower() == "true"),
145148
"VP_LOG_LEVEL": ("log_level", str),
146149
"VP_NO_COLOR": ("no_color", lambda x: x.lower() == "true"),

0 commit comments

Comments
 (0)