Skip to content

Commit

Permalink
Merge pull request #67 from manics/config-exe
Browse files Browse the repository at this point in the history
podman executable can be overridden
  • Loading branch information
manics authored Oct 14, 2023
2 parents 85076c3 + 3455155 commit ffcaf39
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 49 deletions.
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,18 @@ Requires Podman 3+.

Simply include `--engine podman` in the arguments to `repo2docker`:

repo2docker --engine podman repository/to/build
repo2docker --engine podman <repository>

### Using a different Podman executable

repo2podman uses the `podman` command line executable, so it should be possible to substitute any other docker/podman compatible command line tool.

For example, `nerdctl`:

repo2docker --engine podman --PodmanEngine.podman_executable=nerdctl <repository>

`podman-remote`:

export CONTAINER_HOST=ssh://<user>@<host>/home/<user>/podman.sock
export CONTAINER_SSHKEY=$HOME/.ssh/<ssh-private-key>
repo2docker --engine=podman --PodmanEngine.podman_executable=podman-remote <repository>
152 changes: 104 additions & 48 deletions repo2podman/podman.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ def __str__(self):
return s


def exec_podman(args, *, capture, read_timeout=None, break_callback=None):
def exec_podman(args, *, capture, exe="podman", read_timeout=None, break_callback=None):
"""
Execute a podman command
capture:
Expand All @@ -177,7 +177,7 @@ def exec_podman(args, *, capture, read_timeout=None, break_callback=None):
Note podman usually exits with code 125 if a podman error occurred to differentiate
it from the exit code of the container.
"""
cmd = ["podman"] + args
cmd = [exe] + args
log_debug("Executing: {}".format(" ".join(cmd)))
try:
p = execute_cmd(cmd, capture=capture, break_callback=break_callback)
Expand All @@ -194,38 +194,60 @@ def exec_podman(args, *, capture, read_timeout=None, break_callback=None):
raise PodmanCommandError(e, lines) from None


def exec_podman_stream(args, *, read_timeout=None, break_callback=None):
def exec_podman_stream(args, *, exe="podman", read_timeout=None, break_callback=None):
"""
Execute a podman command and stream the output
Passes on CalledProcessError if exit code is not 0
"""
cmd = ["podman"] + args
cmd = [exe] + args
log_debug("Executing: {}".format(" ".join(cmd)))
p = execute_cmd(cmd, capture="both", break_callback=break_callback)
# This will stream the output and also pass any exceptions to the caller
yield from p


def _parse_json_or_jsonl(lines):
"""
Parse an array of lines as JSON or JSONL
"""
is_jsonl = True
for line in lines:
line = line.strip()
if not line or line[0] != "{" or line[-1] != "}":
is_jsonl = False
break
if is_jsonl:
return [json.loads(line) for line in lines]
lines = "".join(lines)
if lines.strip():
return json.loads(lines)
return []


class PodmanContainer(Container):
def __init__(self, cid):
def __init__(self, cid, podman_executable="podman"):
self.id = cid
self._podman_executable = podman_executable
self.reload()

def reload(self):
lines = exec_podman(
["inspect", "--type", "container", "--format", "json", self.id],
capture="stdout",
exe=self._podman_executable,
)
d = json.loads("".join(lines))
d = _parse_json_or_jsonl(lines)
assert len(d) == 1
self.attrs = d[0]
assert self.attrs["Id"].startswith(self.id)

def _exited(self):
status = "\n".join(
exec_podman(
["inspect", "--format={{.State.Status}}", self.id], capture="both"
["inspect", "--format={{.State.Status}}", self.id],
capture="both",
exe=self._podman_executable,
)
)
return status.strip() == "exited"
Expand All @@ -244,6 +266,7 @@ def iter_logs(cid):
try:
for line in exec_podman_stream(
log_command + ["--follow", cid],
exe=self._podman_executable,
read_timeout=2,
break_callback=self._exited,
):
Expand All @@ -254,26 +277,38 @@ def iter_logs(cid):

return iter_logs(self.id)

return "\n".join(exec_podman(log_command + [self.id], capture="both")).encode(
"utf-8"
)
return "\n".join(
exec_podman(
log_command + [self.id], capture="both", exe=self._podman_executable
)
).encode("utf-8")

def kill(self, *, signal="KILL"):
lines = exec_podman(["kill", "--signal", signal, self.id], capture="stdout")
lines = exec_podman(
["kill", "--signal", signal, self.id],
capture="stdout",
exe=self._podman_executable,
)
log_info(lines)

def remove(self):
lines = exec_podman(["rm", self.id], capture="stdout")
lines = exec_podman(
["rm", self.id], capture="stdout", exe=self._podman_executable
)
log_info(lines)

def stop(self, *, timeout=10):
lines = exec_podman(
["stop", "--timeout", str(timeout), self.id], capture="stdout"
["stop", "--timeout", str(timeout), self.id],
capture="stdout",
exe=self._podman_executable,
)
log_info(lines)

def wait(self):
lines = exec_podman(["wait", self.id], capture="stdout")
lines = exec_podman(
["wait", self.id], capture="stdout", exe=self._podman_executable
)
log_info(lines)

@property
Expand All @@ -290,12 +325,6 @@ class PodmanEngine(ContainerEngine):
Podman container engine
"""

def __init__(self, *, parent):
super().__init__(parent=parent)

lines = exec_podman(["info"], capture="stdout")
log_debug(lines)

default_transport = Unicode(
"docker://",
help="""
Expand All @@ -304,6 +333,23 @@ def __init__(self, *, parent):
config=True,
)

podman_executable = Unicode(
"podman",
help="""The podman executable to use for all commands.
For example, you could use an alternative podman/docker compatible command.
Defaults to `podman` on the PATH.
""",
config=True,
)

podman_loglevel = Unicode("", help="Podman log level", config=True)

def __init__(self, *, parent):
super().__init__(parent=parent)

lines = exec_podman(["info"], capture="stdout", exe=self.podman_executable)
log_debug(lines)

def build(
self,
*,
Expand Down Expand Up @@ -351,7 +397,8 @@ def build(
except KeyError:
pass

cmdargs.append("--force-rm")
# Disable for better compatibility with other CLIs
# cmdargs.append("--force-rm")

cmdargs.append("--rm")

Expand Down Expand Up @@ -388,12 +435,16 @@ def build(

lines = execute_cmd(["ls", "-lRa", builddir], capture="stdout")
log_debug(lines)
for line in exec_podman_stream(cmdargs + [builddir]):
for line in exec_podman_stream(
cmdargs + [builddir], exe=self.podman_executable
):
yield line
else:
builddir = path
assert path
for line in exec_podman_stream(cmdargs + [builddir]):
for line in exec_podman_stream(
cmdargs + [builddir], exe=self.podman_executable
):
yield line

def images(self):
Expand All @@ -405,30 +456,32 @@ def remove_local(tags):
if tag.startswith("localhost/"):
yield tag[10:]

lines = exec_podman(["image", "list", "--format", "json"], capture="stdout")
lines = "".join(lines)
lines = exec_podman(
["image", "list", "--format", "json"],
capture="stdout",
exe=self.podman_executable,
)
# Podman returns an array, nerdctl returns JSONL
images = _parse_json_or_jsonl(lines)

if lines.strip():
images = json.loads(lines)
try:
return [
Image(tags=list(remove_local(image["names"]))) for image in images
]
except KeyError:
# Podman 1.9.1+
# Some images may not have a name
return [
Image(tags=list(remove_local(image["Names"])))
for image in images
if "Names" in image
]
return []
try:
return [Image(tags=list(remove_local(image["names"]))) for image in images]
except KeyError:
# Podman 1.9.1+
# Some images may not have a name
return [
Image(tags=list(remove_local(image["Names"])))
for image in images
if "Names" in image
]

def inspect_image(self, image):
lines = exec_podman(
["inspect", "--type", "image", "--format", "json", image], capture="stdout"
["inspect", "--type", "image", "--format", "json", image],
capture="stdout",
exe=self.podman_executable,
)
d = json.loads("".join(lines))
d = _parse_json_or_jsonl(lines)
assert len(d) == 1
tags = d[0]["RepoTags"]
config = d[0]["Config"]
Expand All @@ -448,7 +501,7 @@ def push(self, image_spec):
args = ["push", image_spec, destination]

def iter_out():
for line in exec_podman_stream(args):
for line in exec_podman_stream(args, exe=self.podman_executable):
yield line

return iter_out()
Expand All @@ -472,10 +525,11 @@ def run(
cmdargs.append("--publish-all")

ports = ports or {}
# container-port/protocol:host-port
for k, v in ports.items():
if k.endswith("/tcp"):
k = k[:-4]
cmdargs.extend(["--publish", "{}:{}".format(k, v)])
cmdargs.extend(["--publish", "{}:{}".format(v, k)])

cmdargs.append("--detach")

Expand All @@ -490,21 +544,23 @@ def run(
if remove:
cmdargs.append("--rm")

# TODO: Make this configurable via a config traitlet
cmdargs.append("--log-level=debug")
if self.podman_loglevel:
cmdargs.append(f"--log-level={self.podman_loglevel}")

command = command or []

if kwargs:
raise ValueError("Additional kwargs not supported")

cmdline = cmdargs + [image_spec] + command
lines = exec_podman(cmdline, capture="stdout")
lines = exec_podman(cmdline, capture="stdout", exe=self.podman_executable)

# Note possible race condition:
# If the container exits immediately and remove=True the next line may fail
# since it's not possible to fetch the container details

# If image was pulled the progress logs will also be present
# assert len(lines) == 1, lines
return PodmanContainer(lines[-1].strip())
return PodmanContainer(
lines[-1].strip(), podman_executable=self.podman_executable
)
34 changes: 34 additions & 0 deletions tests/unit/test_podman.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,37 @@ def test_run_detach_stream_exited():
c.remove()
with pytest.raises(PodmanCommandError):
c.reload()


def test_custom_executable(tmp_path):
# Use a wrapper script to log commands
log = tmp_path.joinpath("log")
exe = tmp_path.joinpath("custom_exe.sh")
with exe.open("w") as f:
f.write("#!/bin/sh\n")
f.write(f"echo $@ >> {log}\n")
f.write("exec podman $@\n")
exe.chmod(0o755)

client = PodmanEngine(parent=None)
client.podman_executable = str(exe)
client.podman_loglevel = "debug"
c = client.run(BUSYBOX, command=["id", "-un"])
assert isinstance(c, PodmanContainer)
assert c._podman_executable == str(exe)
cid = c.id

# If image was pulled the progress logs will also be present
out = c.logs().splitlines()
assert out[-1].strip() == b"root", out

c.remove()

with log.open() as f:
lines = f.read().splitlines()
assert lines == [
f"run --detach --log-level=debug {BUSYBOX} id -un",
f"inspect --type container --format json {cid}",
f"logs {cid}",
f"rm {cid}",
]

0 comments on commit ffcaf39

Please sign in to comment.