Podman Support - #17
Conversation
📝 WalkthroughWalkthroughThe CLI now supports Docker and Podman runtime detection, configuration, compose execution, socket mounting, engine startup checks, and configurable template hosts with documented URL resolution. ChangesContainer engine support
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant CLI
participant RuntimeResolver
participant Compose
participant DockerOrPodman
CLI->>RuntimeResolver: resolve configured or detected engine
RuntimeResolver->>DockerOrPodman: probe engine binary
CLI->>Compose: construct project compose command
Compose->>DockerOrPodman: run compose operation
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
commands/db.py (1)
55-68: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winExact-string match can leave a stale, duplicate socket mount after switching container engines.
if mount in contentonly matches when the host-side socket path is byte-identical to what's already in the compose file. Sincecontainer_engineis a global config that can be changed after an agent is provisioned (portabase config engine podman), a subsequentdb add-dbwith adocker-volumeselection computes a differentmount(now pointing at the Podman socket) that won't match the existing Docker-socket line. The code then inserts a second- <mount>entry targeting the same container path (CONTAINER_SOCKET) from a different host source, producing a broken/ambiguous compose file instead of updating the stale entry.🔧 Proposed fix: match/replace by target path instead of exact string
content = compose_path.read_text() - if mount in content: - return + existing = re.search( + rf"^(\s*)- .*:{re.escape(CONTAINER_SOCKET)}\s*$", content, re.MULTILINE + ) + if existing: + if existing.group(0).strip() == f"- {mount}": + return + # A mount to the same container path exists but points at a stale + # host socket (e.g. after switching container engines). Replace it. + content = ( + content[: existing.start()] + + f"{existing.group(1)}- {mount}" + + content[existing.end():] + ) + compose_path.write_text(content) + console.print( + f"[info]ℹ Updated container socket mount to [bold]{mount}[/bold].[/info]" + ) + return🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@commands/db.py` around lines 55 - 68, The compose update logic must identify an existing socket volume by its container target path, not by exact host-side mount text. Update the block around the mount-in-content check and the anchor insertion loop to replace the existing entry targeting CONTAINER_SOCKET with the newly computed mount, while preserving unrelated mounts and inserting the mount only when no socket target exists.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@commands/agent.py`:
- Line 14: Update the docker-volume warning in the agent flow around
socket_mount() to use the resolved container engine instead of hardcoding
“Docker socket.” When Podman is selected, explicitly instruct users to enable
podman.socket, matching the guidance in ensure_docker_socket(), while preserving
the existing Docker warning behavior.
In `@core/config.py`:
- Around line 10-12: Update the TEMPLATE_BASE_URL initialization to treat an
empty PORTABASE_TEMPLATE_URL as unset by falling back to
DEFAULT_TEMPLATE_BASE_URL before applying rstrip("/"), ensuring downstream URL
construction always uses a valid host.
In `@core/utils.py`:
- Around line 119-155: Update the readiness-probe handler inside start_engine to
catch Exception rather than using a bare except, allowing KeyboardInterrupt to
propagate and terminate the wait immediately. Also adjust the Podman/Linux path
so it does not run the startup polling loop when no engine start command was
issued, while preserving readiness checks for engines that were actually
started.
---
Outside diff comments:
In `@commands/db.py`:
- Around line 55-68: The compose update logic must identify an existing socket
volume by its container target path, not by exact host-side mount text. Update
the block around the mount-in-content check and the anchor insertion loop to
replace the existing entry targeting CONTAINER_SOCKET with the newly computed
mount, while preserving unrelated mounts and inserting the mount only when no
socket target exists.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 2b0e2979-e28d-4436-8ff0-a91a0412966a
📒 Files selected for processing (8)
README.mdcommands/agent.pycommands/common.pycommands/config.pycommands/db.pycore/config.pycore/docker.pycore/utils.py
📜 Review details
🧰 Additional context used
🪛 ast-grep (0.44.1)
core/utils.py
[error] 128-128: Command coming from incoming request
Context: subprocess.run(["sudo", "systemctl", "start", "docker"], check=True)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
[error] 130-130: Command coming from incoming request
Context: subprocess.run(["open", "--background", "-a", "Docker"], check=True)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
[error] 132-132: Command coming from incoming request
Context: subprocess.run(["start", "docker"], shell=True, check=True)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
[error] 136-136: Command coming from incoming request
Context: subprocess.run([binary, "machine", "start"], check=True)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
[error] 141-146: Command coming from incoming request
Context: subprocess.run(
[binary, "info"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=True,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
[error] 164-169: Command coming from incoming request
Context: subprocess.run(
[rt["bin"], "info"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=True,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
core/docker.py
[error] 151-151: Use of unsanitized data to create processes
Context: subprocess.run(cmd, cwd=cwd, check=True)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(os-system-unsanitized-data)
[error] 22-27: Command coming from incoming request
Context: subprocess.run(
["podman", "info", "--format", "{{.Host.RemoteSocket.Path}}"],
capture_output=True,
text=True,
check=True,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
[error] 43-48: Command coming from incoming request
Context: subprocess.run(
[binary, "compose", "version"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=True,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
[error] 133-138: Command coming from incoming request
Context: subprocess.run(
[rt["bin"], "network", "inspect", name],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=True,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
[error] 140-144: Command coming from incoming request
Context: subprocess.run(
[rt["bin"], "network", "create", name],
stdout=subprocess.DEVNULL,
check=True,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
[error] 151-151: Command coming from incoming request
Context: subprocess.run(cmd, cwd=cwd, check=True)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
🪛 OpenGrep (1.25.0)
core/utils.py
[ERROR] 133-133: Dynamic command passed to subprocess with shell=True. Use a command list without shell=True, or use shlex.quote() to sanitize input.
(coderabbit.command-injection.python-shell-true)
🪛 Ruff (0.15.21)
commands/common.py
[error] 44-44: subprocess call: check for execution of untrusted input
(S603)
[warning] 44-44: subprocess.run without explicit check argument
Add explicit check=False
(PLW1510)
commands/db.py
[warning] 73-73: String contains ambiguous ℹ (INFORMATION SOURCE). Did you mean i (LATIN SMALL LETTER I)?
(RUF001)
commands/config.py
[error] 54-54: f-string without any placeholders
Remove extraneous f prefix
(F541)
core/utils.py
[error] 129-129: Starting a process with a partial executable path
(S607)
[error] 131-131: Starting a process with a partial executable path
(S607)
[error] 133-133: subprocess call with shell=True identified, security issue
(S602)
[error] 133-133: Starting a process with a partial executable path
(S607)
[warning] 134-136: Use elif instead of else then if, to reduce indentation
Convert to elif
(PLR5501)
[error] 137-137: subprocess call: check for execution of untrusted input
(S603)
[error] 142-142: subprocess call: check for execution of untrusted input
(S603)
[warning] 149-149: Consider moving this statement to an else block
(TRY300)
[error] 150-150: Do not use bare except
(E722)
[warning] 152-152: Do not catch blind exception: Exception
(BLE001)
[error] 165-165: subprocess call: check for execution of untrusted input
(S603)
[warning] 176-177: Use a single if statement instead of nested if statements
(SIM102)
[warning] 181-181: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
[warning] 182-182: Do not catch blind exception: Exception
(BLE001)
core/docker.py
[error] 24-24: Starting a process with a partial executable path
(S607)
[error] 32-33: try-except-pass detected, consider logging the exception
(S110)
[warning] 32-32: Do not catch blind exception: Exception
(BLE001)
[error] 44-44: subprocess call: check for execution of untrusted input
(S603)
[warning] 50-50: Consider moving this statement to an else block
(TRY300)
[warning] 51-51: Do not catch blind exception: Exception
(BLE001)
[warning] 62-65: Use ternary operator socket = _detect_podman_socket() if engine == "podman" else "/var/run/docker.sock" instead of if-else-block
Replace if-else-block with socket = _detect_podman_socket() if engine == "podman" else "/var/run/docker.sock"
(SIM108)
[warning] 82-82: Using the global statement to update _runtime is discouraged
(PLW0603)
[warning] 82-82: Using the global statement to update _runtime is discouraged
(PLW0603)
[error] 134-134: subprocess call: check for execution of untrusted input
(S603)
[error] 141-141: subprocess call: check for execution of untrusted input
(S603)
[error] 152-152: subprocess call: check for execution of untrusted input
(S603)
[warning] 155-155: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
🔇 Additional comments (7)
README.md (1)
42-66: LGTM!core/docker.py (1)
1-71: LGTM!Also applies to: 74-129, 131-146, 148-155
core/utils.py (2)
43-43: LGTM!Also applies to: 52-52, 65-66
158-183: LGTM!commands/config.py (1)
24-47: LGTM!Also applies to: 50-56
commands/common.py (1)
7-7: LGTM!Also applies to: 44-44
commands/db.py (1)
13-13: LGTM!Also applies to: 36-53
| TEMPLATE_BASE_URL = os.environ.get( | ||
| "PORTABASE_TEMPLATE_URL", DEFAULT_TEMPLATE_BASE_URL | ||
| ).rstrip("/") |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Treat an empty template URL as unset or reject it explicitly.
If PORTABASE_TEMPLATE_URL="", os.environ.get returns the empty string, so core/network.py builds URLs such as /latest/template.yaml; requests.get then fails instead of using the documented default host. Use os.environ.get(...) or DEFAULT_TEMPLATE_BASE_URL, or validate and report the configuration error during startup.
Proposed fix
TEMPLATE_BASE_URL = os.environ.get(
"PORTABASE_TEMPLATE_URL", DEFAULT_TEMPLATE_BASE_URL
-).rstrip("/")
+) or DEFAULT_TEMPLATE_BASE_URL
+TEMPLATE_BASE_URL = TEMPLATE_BASE_URL.rstrip("/")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| TEMPLATE_BASE_URL = os.environ.get( | |
| "PORTABASE_TEMPLATE_URL", DEFAULT_TEMPLATE_BASE_URL | |
| ).rstrip("/") | |
| TEMPLATE_BASE_URL = os.environ.get( | |
| "PORTABASE_TEMPLATE_URL", DEFAULT_TEMPLATE_BASE_URL | |
| ) or DEFAULT_TEMPLATE_BASE_URL | |
| TEMPLATE_BASE_URL = TEMPLATE_BASE_URL.rstrip("/") |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@core/config.py` around lines 10 - 12, Update the TEMPLATE_BASE_URL
initialization to treat an empty PORTABASE_TEMPLATE_URL as unset by falling back
to DEFAULT_TEMPLATE_BASE_URL before applying rstrip("/"), ensuring downstream
URL construction always uses a valid host.
| def start_engine(rt): | ||
| """Attempts to start the container engine based on the engine and OS.""" | ||
| engine = rt["engine"] | ||
| binary = rt["bin"] | ||
| label = engine.capitalize() | ||
| os_type = platform.system() | ||
|
|
||
| try: | ||
| if os_type == "Linux": | ||
| subprocess.run(["sudo", "systemctl", "start", "docker"], check=True) | ||
| elif os_type == "Darwin": | ||
| subprocess.run(["open", "--background", "-a", "Docker"], check=True) | ||
| elif os_type == "Windows": | ||
| subprocess.run(["start", "docker"], shell=True, check=True) | ||
|
|
||
| console.print("[info]Waiting for Docker to start...[/info]") | ||
| if engine == "docker": | ||
| if os_type == "Linux": | ||
| subprocess.run(["sudo", "systemctl", "start", "docker"], check=True) | ||
| elif os_type == "Darwin": | ||
| subprocess.run(["open", "--background", "-a", "Docker"], check=True) | ||
| elif os_type == "Windows": | ||
| subprocess.run(["start", "docker"], shell=True, check=True) | ||
| else: # podman | ||
| # Podman is daemonless on Linux; only the macOS/Windows VM needs starting. | ||
| if os_type in ("Darwin", "Windows"): | ||
| subprocess.run([binary, "machine", "start"], check=True) | ||
|
|
||
| console.print(f"[info]Waiting for {label} to start...[/info]") | ||
| for _ in range(10): | ||
| try: | ||
| subprocess.run( | ||
| ["docker", "info"], | ||
| [binary, "info"], | ||
| stdout=subprocess.DEVNULL, | ||
| stderr=subprocess.DEVNULL, | ||
| check=True, | ||
| ) | ||
| console.print("[success]✔ Docker started successfully.[/success]") | ||
| console.print(f"[success]✔ {label} started successfully.[/success]") | ||
| return True | ||
| except: | ||
| time.sleep(2) | ||
| except Exception as e: | ||
| console.print(f"[danger]✖ Failed to start Docker:[/danger] {e}") | ||
| console.print(f"[danger]✖ Failed to start {label}:[/danger] {e}") | ||
|
|
||
| return False |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Bare except: swallows KeyboardInterrupt while waiting for the engine to start.
Line 150 catches everything, not just failed readiness probes. If the user presses Ctrl+C during the "Waiting for {label} to start..." loop, the interrupt is silently absorbed and the loop keeps sleeping/retrying instead of exiting.
Separately, for Podman on Linux (daemonless — no start command is issued), the function still runs the full 10×2s polling loop even though nothing was started, giving a misleading "waiting to start" message if podman is actually broken for an unrelated reason.
🔧 Proposed fix for the bare except
console.print(f"[info]Waiting for {label} to start...[/info]")
for _ in range(10):
try:
subprocess.run(
[binary, "info"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=True,
)
console.print(f"[success]✔ {label} started successfully.[/success]")
return True
- except:
+ except subprocess.CalledProcessError:
time.sleep(2)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def start_engine(rt): | |
| """Attempts to start the container engine based on the engine and OS.""" | |
| engine = rt["engine"] | |
| binary = rt["bin"] | |
| label = engine.capitalize() | |
| os_type = platform.system() | |
| try: | |
| if os_type == "Linux": | |
| subprocess.run(["sudo", "systemctl", "start", "docker"], check=True) | |
| elif os_type == "Darwin": | |
| subprocess.run(["open", "--background", "-a", "Docker"], check=True) | |
| elif os_type == "Windows": | |
| subprocess.run(["start", "docker"], shell=True, check=True) | |
| console.print("[info]Waiting for Docker to start...[/info]") | |
| if engine == "docker": | |
| if os_type == "Linux": | |
| subprocess.run(["sudo", "systemctl", "start", "docker"], check=True) | |
| elif os_type == "Darwin": | |
| subprocess.run(["open", "--background", "-a", "Docker"], check=True) | |
| elif os_type == "Windows": | |
| subprocess.run(["start", "docker"], shell=True, check=True) | |
| else: # podman | |
| # Podman is daemonless on Linux; only the macOS/Windows VM needs starting. | |
| if os_type in ("Darwin", "Windows"): | |
| subprocess.run([binary, "machine", "start"], check=True) | |
| console.print(f"[info]Waiting for {label} to start...[/info]") | |
| for _ in range(10): | |
| try: | |
| subprocess.run( | |
| ["docker", "info"], | |
| [binary, "info"], | |
| stdout=subprocess.DEVNULL, | |
| stderr=subprocess.DEVNULL, | |
| check=True, | |
| ) | |
| console.print("[success]✔ Docker started successfully.[/success]") | |
| console.print(f"[success]✔ {label} started successfully.[/success]") | |
| return True | |
| except: | |
| time.sleep(2) | |
| except Exception as e: | |
| console.print(f"[danger]✖ Failed to start Docker:[/danger] {e}") | |
| console.print(f"[danger]✖ Failed to start {label}:[/danger] {e}") | |
| return False | |
| def start_engine(rt): | |
| """Attempts to start the container engine based on the engine and OS.""" | |
| engine = rt["engine"] | |
| binary = rt["bin"] | |
| label = engine.capitalize() | |
| os_type = platform.system() | |
| try: | |
| if engine == "docker": | |
| if os_type == "Linux": | |
| subprocess.run(["sudo", "systemctl", "start", "docker"], check=True) | |
| elif os_type == "Darwin": | |
| subprocess.run(["open", "--background", "-a", "Docker"], check=True) | |
| elif os_type == "Windows": | |
| subprocess.run(["start", "docker"], shell=True, check=True) | |
| else: # podman | |
| # Podman is daemonless on Linux; only the macOS/Windows VM needs starting. | |
| if os_type in ("Darwin", "Windows"): | |
| subprocess.run([binary, "machine", "start"], check=True) | |
| console.print(f"[info]Waiting for {label} to start...[/info]") | |
| for _ in range(10): | |
| try: | |
| subprocess.run( | |
| [binary, "info"], | |
| stdout=subprocess.DEVNULL, | |
| stderr=subprocess.DEVNULL, | |
| check=True, | |
| ) | |
| console.print(f"[success]✔ {label} started successfully.[/success]") | |
| return True | |
| except subprocess.CalledProcessError: | |
| time.sleep(2) | |
| except Exception as e: | |
| console.print(f"[danger]✖ Failed to start {label}:[/danger] {e}") | |
| return False |
🧰 Tools
🪛 ast-grep (0.44.1)
[error] 128-128: Command coming from incoming request
Context: subprocess.run(["sudo", "systemctl", "start", "docker"], check=True)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
[error] 130-130: Command coming from incoming request
Context: subprocess.run(["open", "--background", "-a", "Docker"], check=True)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
[error] 132-132: Command coming from incoming request
Context: subprocess.run(["start", "docker"], shell=True, check=True)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
[error] 136-136: Command coming from incoming request
Context: subprocess.run([binary, "machine", "start"], check=True)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
[error] 141-146: Command coming from incoming request
Context: subprocess.run(
[binary, "info"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=True,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
🪛 OpenGrep (1.25.0)
[ERROR] 133-133: Dynamic command passed to subprocess with shell=True. Use a command list without shell=True, or use shlex.quote() to sanitize input.
(coderabbit.command-injection.python-shell-true)
🪛 Ruff (0.15.21)
[error] 129-129: Starting a process with a partial executable path
(S607)
[error] 131-131: Starting a process with a partial executable path
(S607)
[error] 133-133: subprocess call with shell=True identified, security issue
(S602)
[error] 133-133: Starting a process with a partial executable path
(S607)
[warning] 134-136: Use elif instead of else then if, to reduce indentation
Convert to elif
(PLR5501)
[error] 137-137: subprocess call: check for execution of untrusted input
(S603)
[error] 142-142: subprocess call: check for execution of untrusted input
(S603)
[warning] 149-149: Consider moving this statement to an else block
(TRY300)
[error] 150-150: Do not use bare except
(E722)
[warning] 152-152: Do not catch blind exception: Exception
(BLE001)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@core/utils.py` around lines 119 - 155, Update the readiness-probe handler
inside start_engine to catch Exception rather than using a bare except, allowing
KeyboardInterrupt to propagate and terminate the wait immediately. Also adjust
the Podman/Linux path so it does not run the startup polling loop when no engine
start command was issued, while preserving readiness checks for engines that
were actually started.
|
Hi @aneillans, |
Removed the 'Requirements' section detailing container engine options and template configuration.
| # Allow pointing the CLI at a different template host (air-gapped / private / | ||
| # self-hosted setups) via the PORTABASE_TEMPLATE_URL environment variable. |
| # In-container path the agent app always talks to. Podman's socket is | ||
| # Docker-API-compatible, so we map the host socket to this path regardless | ||
| # of the engine. |
| # of the engine. | ||
| CONTAINER_SOCKET = "/var/run/docker.sock" | ||
|
|
||
| # Memoized runtime description so we probe subprocess only once per invocation. |
There was a problem hiding this comment.
remove this
not
CONTAINER_SOCKET = "/var/run/docker.sock"
| # Fall back to the plugin form; the actual command will surface any | ||
| # missing-provider error to the user at run time. |
| else: # podman | ||
| # Podman is daemonless on Linux; only the macOS/Windows VM needs starting. |
This adds in Podman detection and handling for the CLI, as a companion to the changes to the agent.
It also has a capability to allow pulling a local templates config for testing.
Tested locally, and I've been able to pull and standup an agent in podman, and complete backups (including a Postgres one).
Summary by CodeRabbit