Compare commits
No commits in common. "main" and "v1.0.0" have entirely different histories.
4 changed files with 3 additions and 139 deletions
22
CHANGELOG.md
22
CHANGELOG.md
|
|
@ -1,27 +1,5 @@
|
||||||
# Release notes
|
# Release notes
|
||||||
|
|
||||||
## 1.0.1 — 2026-10-29
|
|
||||||
|
|
||||||
`podman stop` now stops a running agent container immediately instead of
|
|
||||||
after a 10-second SIGTERM timeout followed by SIGKILL.
|
|
||||||
|
|
||||||
### Why
|
|
||||||
|
|
||||||
Containers used `sleep infinity` as PID 1. The kernel does not apply
|
|
||||||
default signal dispositions to PID 1, and `sleep` has no SIGTERM handler,
|
|
||||||
so the stop signal was silently ignored and every stop fell through to
|
|
||||||
SIGKILL. PID 1 now traps TERM (and INT) itself and exits on request:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
trap "exit 0" TERM INT; sleep infinity & wait
|
|
||||||
```
|
|
||||||
|
|
||||||
### Upgrading
|
|
||||||
|
|
||||||
The PID 1 command is baked in when a container is created, so existing
|
|
||||||
containers are detected on the next run and recreated automatically.
|
|
||||||
Nothing in the container home is touched.
|
|
||||||
|
|
||||||
## 1.0.0 — 2026-10-29
|
## 1.0.0 — 2026-10-29
|
||||||
|
|
||||||
Containers now run with rootless Podman instead of Docker.
|
Containers now run with rootless Podman instead of Docker.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
# Fully qualified registry: Podman on distros without an unqualified search
|
FROM archlinux:latest
|
||||||
# registry configured (e.g. Arch or Ubuntu) cannot resolve the short name.
|
|
||||||
FROM docker.io/library/archlinux:latest
|
|
||||||
|
|
||||||
ARG USERNAME=dev
|
ARG USERNAME=dev
|
||||||
ARG UID=1000
|
ARG UID=1000
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ socket goes away.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
You need rootless Podman on the host. On Arch Linux the packages `podman` and `passt` (or `slirp4netns`) are enough; your user also needs subuid/subgid entries, which `usermod --add-subuids 100000-165535 --add-subgids 100000-165535 <user>` sets up if the package did not (accounts created before shadow 4.11.1-3 get no entries automatically — see [Podman: Set subuid and subgid](https://wiki.archlinux.org/title/Podman#Set_subuid_and_subgid)):
|
You need rootless Podman on the host. On Arch Linux the packages `podman` and `passt` (or `slirp4netns`) are enough; your user also needs subuid/subgid entries, which `usermod --add-subuids 100000-165535 --add-subgids 100000-165535 <user>` sets up if the package did not:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo pacman -S podman passt
|
sudo pacman -S podman passt
|
||||||
|
|
|
||||||
|
|
@ -179,12 +179,6 @@ class AgentContainer:
|
||||||
)
|
)
|
||||||
self.remove_container()
|
self.remove_container()
|
||||||
|
|
||||||
if self.container_exists() and not self._container_cmd_matches():
|
|
||||||
logger.info(
|
|
||||||
"Recreating container: it predates the SIGTERM-aware PID 1"
|
|
||||||
)
|
|
||||||
self.remove_container()
|
|
||||||
|
|
||||||
if not self.container_exists():
|
if not self.container_exists():
|
||||||
self.create_container()
|
self.create_container()
|
||||||
|
|
||||||
|
|
@ -369,89 +363,7 @@ class AgentContainer:
|
||||||
== 0
|
== 0
|
||||||
)
|
)
|
||||||
|
|
||||||
# Subordinate ID block size; rootless Podman needs at least 65536 IDs so
|
|
||||||
# every container UID is mappable. 100000 is the default first block the
|
|
||||||
# shadow suite allocates on Arch (and the number the Arch Wiki suggests).
|
|
||||||
SUBID_SIZE = 65536
|
|
||||||
SUBID_START = 100000
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _free_subid_range(cls) -> str:
|
|
||||||
"""Return the first unclaimed 65536-ID block as "start-end".
|
|
||||||
|
|
||||||
Ranges in /etc/subuid and /etc/subgid must never overlap between
|
|
||||||
users (a shared range would let one user impersonate another inside
|
|
||||||
containers), so both files' claims are considered.
|
|
||||||
"""
|
|
||||||
taken = []
|
|
||||||
for path in ("/etc/subuid", "/etc/subgid"):
|
|
||||||
try:
|
|
||||||
with open(path) as f:
|
|
||||||
for line in f:
|
|
||||||
parts = line.split(":")
|
|
||||||
if len(parts) < 3:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
taken.append(
|
|
||||||
(int(parts[1]), int(parts[1]) + int(parts[2]))
|
|
||||||
)
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
taken.sort()
|
|
||||||
start = cls.SUBID_START
|
|
||||||
for lo, hi in taken:
|
|
||||||
if hi <= start:
|
|
||||||
continue
|
|
||||||
if lo - start >= cls.SUBID_SIZE:
|
|
||||||
break
|
|
||||||
start = max(start, hi)
|
|
||||||
return f"{start}-{start + cls.SUBID_SIZE - 1}"
|
|
||||||
|
|
||||||
def _subid_guard(self) -> None:
|
|
||||||
"""Warn when rootless Podman has no subuid/subgid ranges for this user.
|
|
||||||
|
|
||||||
Without a range in /etc/subuid and /etc/subgid, Podman falls back to a
|
|
||||||
single-UID user namespace: the container only has the invoking user's
|
|
||||||
UID, and chown to any other UID fails with EINVAL. This breaks e.g.
|
|
||||||
pacman's download sandbox, which chowns its download directory to the
|
|
||||||
'alpm' user. Distros like Fedora allocate ranges for every user
|
|
||||||
automatically, but accounts created manually (e.g. on Arch) may not
|
|
||||||
have one.
|
|
||||||
"""
|
|
||||||
if self.RUNTIME != "podman" or os.geteuid() == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
def has_entry(path: str) -> bool:
|
|
||||||
try:
|
|
||||||
with open(path) as f:
|
|
||||||
return any(line.split(":", 1)[0] == self.host_username for line in f)
|
|
||||||
except FileNotFoundError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
missing = [
|
|
||||||
p for p in ("/etc/subuid", "/etc/subgid") if not has_entry(p)
|
|
||||||
]
|
|
||||||
if not missing:
|
|
||||||
return
|
|
||||||
|
|
||||||
range_str = self._free_subid_range()
|
|
||||||
logger.error(
|
|
||||||
f"No subuid/subgid entry for user '{self.host_username}' in"
|
|
||||||
" /etc/subuid, /etc/subgid — rootless Podman has only a single UID"
|
|
||||||
" available, so operations inside containers that chown to other"
|
|
||||||
" UIDs (e.g. pacman's alpm download sandbox) fail with 'Invalid"
|
|
||||||
" argument'. Fix once per user:\n"
|
|
||||||
f" sudo usermod --add-subuids {range_str} --add-subgids {range_str} {self.host_username}\n"
|
|
||||||
" podman system migrate\n"
|
|
||||||
" See https://wiki.archlinux.org/title/Podman#Set_subuid_and_subgid"
|
|
||||||
" or https://github.com/containers/podman/blob/main/docs/tutorials/rootless_tutorial.md"
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
def build_image(self, no_cache: bool = False, pull: bool = False) -> None:
|
def build_image(self, no_cache: bool = False, pull: bool = False) -> None:
|
||||||
self._subid_guard()
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Building image '{self.IMAGE}' with user {self.host_username} ({self.host_uid}:{self.host_gid})"
|
f"Building image '{self.IMAGE}' with user {self.host_username} ({self.host_uid}:{self.host_gid})"
|
||||||
)
|
)
|
||||||
|
|
@ -519,11 +431,7 @@ class AgentContainer:
|
||||||
self.IMAGE,
|
self.IMAGE,
|
||||||
"sh",
|
"sh",
|
||||||
"-c",
|
"-c",
|
||||||
# PID 1 ignores signals with default dispositions, so a bare
|
"sleep infinity",
|
||||||
# `sleep infinity` never reacts to SIGTERM and every
|
|
||||||
# `podman stop` times out into SIGKILL. Trapping TERM (and
|
|
||||||
# INT) makes stop requests end the container immediately.
|
|
||||||
'trap "exit 0" TERM INT; sleep infinity & wait',
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -565,26 +473,6 @@ class AgentContainer:
|
||||||
).stdout
|
).stdout
|
||||||
return "AGENT_CONTAINER=1" in env
|
return "AGENT_CONTAINER=1" in env
|
||||||
|
|
||||||
def _container_cmd_matches(self) -> bool:
|
|
||||||
"""Whether the container runs the current PID 1 command.
|
|
||||||
|
|
||||||
The PID 1 entrypoint is baked in at create time and can only change
|
|
||||||
by recreating the container. Comparing against Config.Cmd catches
|
|
||||||
containers created before the SIGTERM-aware fix."""
|
|
||||||
cmd = subprocess.run(
|
|
||||||
[
|
|
||||||
self.RUNTIME,
|
|
||||||
"container",
|
|
||||||
"inspect",
|
|
||||||
"-f",
|
|
||||||
"{{json .Config.Cmd}}",
|
|
||||||
self.container_name,
|
|
||||||
],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
).stdout.strip()
|
|
||||||
return "TERM INT" in cmd
|
|
||||||
|
|
||||||
def start_container(self) -> bool:
|
def start_container(self) -> bool:
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
[self.RUNTIME, "start", self.container_name],
|
[self.RUNTIME, "start", self.container_name],
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue