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
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ _probe_timeout() { # seconds cmd...; portable across GNU timeout, macOS Homebre
else
"$@" &
local cmd_pid=$!
(sleep "$secs" && kill "$cmd_pid" 2> /dev/null) &
(sleep "$secs" && kill "$cmd_pid" 2> /dev/null) > /dev/null 2>&1 &
local watchdog_pid=$!
wait "$cmd_pid" 2> /dev/null
local rc=$?
Expand Down Expand Up @@ -100,11 +100,12 @@ for rt in podman docker; do
echo "PROBE: ${rt}-runtime → ✗ (gateway socket missing at $sock — container gateway not running)"
continue
fi
if _probe_timeout 15 "$rt" info > /dev/null 2>"${TMPDIR:-/tmp}/$rt-probe.err"; then
# Held in a variable, not a file: /tmp is read-only inside the sandbox.
out=$(_probe_timeout 15 "$rt" info 2>&1 > /dev/null); rc=$?
if [ $rc -eq 0 ]; then
echo "PROBE: ${rt}-runtime → ✓ ($rt reaches the container gateway at $sock)"
else
rc=$?
err=$(head -1 "${TMPDIR:-/tmp}/$rt-probe.err")
err=$(printf '%s\n' "$out" | head -1)
case "$rc" in
137|143)
# The shell-fallback branch of _probe_timeout kills the child with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ else
ssh-*) echo "PROBE: signing-key → ✓ (user.signingkey is a literal key, nothing to read)" ;;
*)
key="${key/#\~/$HOME}"
if head -c 1 "$key" >/dev/null 2>"${TMPDIR:-/tmp}/signing-key.err"; then
# Held in a variable, not a file: /tmp is read-only inside the sandbox.
out=$(head -c 1 "$key" 2>&1); rc=$?
if [ $rc -eq 0 ]; then
echo "PROBE: signing-key → ✓ ($key readable inside sandbox)"
else
echo "PROBE: signing-key → ✗ ($key not readable inside sandbox: $(head -1 "${TMPDIR:-/tmp}/signing-key.err"))"
echo "PROBE: signing-key → ✗ ($key not readable inside sandbox: $(printf '%s\n' "$out" | head -1))"
fi ;;
esac
fi
Expand Down
94 changes: 94 additions & 0 deletions plugins/magpie-setup/skills/isolated-setup-doctor/tests/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Shared test helpers for isolated-setup-doctor probe tests."""

from __future__ import annotations

import os
import shutil
import subprocess
from pathlib import Path


def find_bash() -> str | None:
"""Finds a functional bash executable, preferring Git Bash over WSL on Windows."""
if os.name == "nt":
for candidate in [
Path("C:/Program Files/Git/bin/bash.exe"),
Path("C:/Program Files/Git/usr/bin/bash.exe"),
Path("C:/Program Files (x86)/Git/bin/bash.exe"),
]:
if candidate.is_file():
return str(candidate)
w = shutil.which("bash")
if w and "system32" not in w.lower():
return w
return None
return shutil.which("bash") or "bash"


def to_posix(path: Path | str) -> str:
"""Converts a Path or path string to POSIX format, converting Windows drive letters."""
s = Path(path).as_posix()
if os.name == "nt" and len(s) >= 2 and s[1] == ":":
return f"/{s[0].lower()}{s[2:]}"
return s


def run_probe_script(
probe_path: Path,
env_vars: dict[str, str],
bin_dir: Path | None = None,
cwd: Path | None = None,
) -> str:
"""Runs a probe script with proper PATH, POSIX paths, and UTF-8 encoding."""
bash = find_bash()
if not bash:
raise RuntimeError("bash not found")

# A minimal base, not the host environment: the host's CONTAINER_HOST /
# DOCKER_HOST, or a gateway running on this machine, must not decide a test.
keep = ["PATH", "HOME", "LANG", "LC_ALL", "TMPDIR"]
if os.name == "nt":
keep += ["SYSTEMROOT", "COMSPEC", "PATHEXT", "TEMP", "TMP", "USERPROFILE"]
env = {name: os.environ[name] for name in keep if name in os.environ}
env.update(env_vars)

if bin_dir is not None:
env["PATH"] = f"{bin_dir}{os.pathsep}{env.get('PATH', '')}"

if os.name == "nt":
probe_posix = to_posix(probe_path)
if bin_dir is not None:
bin_posix = to_posix(bin_dir)
cmd = f'export PATH="{bin_posix}:$PATH"; bash "{probe_posix}"'
else:
cmd = f'bash "{probe_posix}"'
args = [bash, "-c", cmd]
else:
args = [bash, str(probe_path)]

res = subprocess.run(
args,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
env=env,
cwd=cwd,
)
return res.stdout
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,21 @@ def setUp(self) -> None:
# ssh-add never connects to it. A sandbox may refuse bind(2), so fall
# back to the session's own agent socket.
self.sock_path = self.tmp / "agent.sock"
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.addCleanup(sock.close)
try:
sock.bind(str(self.sock_path))
except OSError:
if not hasattr(socket, "AF_UNIX"):
existing = os.environ.get("SSH_AUTH_SOCK", "")
if not (existing and Path(existing).is_socket()):
self.skipTest("cannot bind a unix socket and no SSH_AUTH_SOCK to reuse")
self.skipTest("AF_UNIX is not supported on this platform and no SSH_AUTH_SOCK to reuse")
self.sock_path = Path(existing)
else:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.addCleanup(sock.close)
try:
sock.bind(str(self.sock_path))
except OSError:
existing = os.environ.get("SSH_AUTH_SOCK", "")
if not (existing and Path(existing).is_socket()):
self.skipTest("cannot bind a unix socket and no SSH_AUTH_SOCK to reuse")
self.sock_path = Path(existing)

def fake_ssh_add(self, output: str, rc: int) -> None:
script = self.bin / "ssh-add"
Expand All @@ -69,7 +75,14 @@ def run_probe(self) -> str:
"SSH_AUTH_SOCK": str(self.sock_path),
"TMPDIR": str(self.tmp),
}
result = subprocess.run(["bash", str(PROBE)], env=env, capture_output=True, text=True, check=True)
result = subprocess.run(
["bash", str(PROBE)],
env=env,
capture_output=True,
text=True,
encoding="utf-8",
check=True,
)
return result.stdout.strip()

def test_counts_listed_identities(self) -> None:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Probe 3 reports container runtime status, and writes no file.

Inside the sandbox /tmp is read-only. The probe holds any error output in a
variable rather than redirecting to a file under /tmp.
"""

from __future__ import annotations

import json
import socket
import stat
import sys
import tempfile
import time
import unittest
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
from helpers import find_bash, run_probe_script, to_posix

PROBE = Path(__file__).resolve().parents[1] / "scripts" / "probe-3-container-gateway.sh"
OLD_PODMAN_ERR = Path("/tmp/podman-probe.err")
OLD_DOCKER_ERR = Path("/tmp/docker-probe.err")


class ProbeThreeContainerGateway(unittest.TestCase):
def setUp(self) -> None:
self.bash = find_bash()
if not self.bash:
self.skipTest("bash is not available on this platform")
self._tmp = tempfile.TemporaryDirectory()
self.tmp = Path(self._tmp.name)
self.bin = self.tmp / "bin"
self.bin.mkdir()
self.addCleanup(self._tmp.cleanup)

def fake_cmd(self, name: str, body: str = "exit 0\n") -> Path:
script = self.bin / name
script.write_text(f"#!/bin/sh\n{body}")
script.chmod(script.stat().st_mode | stat.S_IXUSR)
return script

def setup_stub_gateway(self, serving: list[str]) -> None:
gw_pkg = self.tmp / "tools" / "container-gateway" / "src" / "container_gateway"
gw_pkg.mkdir(parents=True, exist_ok=True)
(gw_pkg / "__init__.py").write_text("", encoding="utf-8")
status_payload = json.dumps({"running": True, "serving": serving})
(gw_pkg / "__main__.py").write_text(
f"import json\nprint({status_payload!r})\n",
encoding="utf-8",
)

def bind_unix_socket(self, name: str) -> Path:
if not hasattr(socket, "AF_UNIX"):
self.skipTest("AF_UNIX is not supported on this platform")
sock_path = self.tmp / name
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.addCleanup(sock.close)
try:
sock.bind(str(sock_path))
except OSError as e:
self.skipTest(f"cannot bind unix socket: {e}")
return sock_path

def run_probe(
self,
env_overrides: dict[str, str] | None = None,
cwd: Path | None = None,
) -> str:
env = {
"TMPDIR": str(self.tmp),
"HOME": str(self.tmp),
**(env_overrides or {}),
}
return run_probe_script(PROBE, env, bin_dir=self.bin, cwd=cwd).strip()

def test_runtimes_not_on_path(self) -> None:
out = self.run_probe(
{
"PATH": str(self.bin),
"CONTAINER_HOST": "",
"DOCKER_HOST": "",
}
)
self.assertIn("PROBE: podman-runtime → ⊘ (podman not on PATH)", out)
self.assertIn("PROBE: docker-runtime → ⊘ (docker not on PATH)", out)

def test_host_env_unset(self) -> None:
self.fake_cmd("podman")
out = self.run_probe({"CONTAINER_HOST": ""})
self.assertIn(
"PROBE: podman-runtime → ✗ (CONTAINER_HOST unset — gateway not wired into settings)",
out,
)

def test_relative_socket_url(self) -> None:
self.fake_cmd("podman")
out = self.run_probe({"CONTAINER_HOST": "unix://run/podman.sock"})
self.assertIn(
"PROBE: podman-runtime → ✗ (CONTAINER_HOST=unix://run/podman.sock is not absolute",
out,
)

def test_gateway_not_running(self) -> None:
self.fake_cmd("podman")
sock = "/var/run/podman.sock"
out = self.run_probe({"CONTAINER_HOST": f"unix://{sock}"})
self.assertIn(
f"PROBE: podman-runtime → ✗ (gateway socket missing at {sock} — container gateway not running)",
out,
)

def test_writes_nothing_under_tmp(self) -> None:
Comment thread
potiuk marked this conversation as resolved.
self.setup_stub_gateway(["podman"])
sock_path = self.bind_unix_socket("podman.sock")
posix_sock = to_posix(sock_path)
self.fake_cmd(
"podman",
'printf "connect: operation not permitted\\n" >&2\nexit 1\n',
)
started = time.time()
out = self.run_probe(
{"CONTAINER_HOST": f"unix://{posix_sock}"},
cwd=self.tmp,
)
self.assertIn(
f"PROBE: podman-runtime → ✗ (connect to {posix_sock} denied — add it to sandbox.network.allowUnixSockets)",
out,
)
if OLD_PODMAN_ERR.exists():
self.assertLess(OLD_PODMAN_ERR.stat().st_mtime, started)
if OLD_DOCKER_ERR.exists():
self.assertLess(OLD_DOCKER_ERR.stat().st_mtime, started)
self.assertFalse((self.tmp / "podman-probe.err").exists())
self.assertFalse((self.tmp / "docker-probe.err").exists())

def test_docker_stdout_does_not_mask_stderr_error(self) -> None:
self.setup_stub_gateway(["docker"])
sock_path = self.bind_unix_socket("docker.sock")
posix_sock = to_posix(sock_path)
self.fake_cmd(
"docker",
'printf "Client: Docker Engine - Community\\n Version: 25.0.0\\n"\n'
'printf "connect: 502 Bad Gateway\\n" >&2\n'
"exit 1\n",
)
out = self.run_probe(
{"DOCKER_HOST": f"unix://{posix_sock}"},
cwd=self.tmp,
)
self.assertIn(
"PROBE: docker-runtime → ✗ (gateway up, backend down: connect: 502 Bad Gateway)",
out,
)
self.assertNotIn("Client:", out)


if __name__ == "__main__":
unittest.main()
Loading
Loading