Add opt-in root access, persistent containers and per-project packages
This commit is contained in:
17
.claudio
17
.claudio
@@ -19,6 +19,23 @@ MOUNTS=(
|
|||||||
GUI=0
|
GUI=0
|
||||||
AUDIO=0
|
AUDIO=0
|
||||||
|
|
||||||
|
# ROOT=1: passwordless sudo inside the container (e.g. to apt-get install tools).
|
||||||
|
# System changes are lost when the container exits, unless PERSIST=1.
|
||||||
|
ROOT=0
|
||||||
|
|
||||||
|
# PERSIST=1: keep this project's container between runs, so system changes
|
||||||
|
# (packages installed with sudo, files outside mounts) survive.
|
||||||
|
# It is recreated, losing them, when the settings here or the image
|
||||||
|
# change. `claudio reset` removes it.
|
||||||
|
PERSIST=0
|
||||||
|
|
||||||
|
# Extra Debian packages, built into a per-project image. Unlike changes made
|
||||||
|
# with sudo, these survive `claudio update` and PERSIST recreations.
|
||||||
|
PACKAGES=(
|
||||||
|
# python3
|
||||||
|
# nodejs
|
||||||
|
)
|
||||||
|
|
||||||
# Extra `podman run` options (env vars, ports, resources, ...).
|
# Extra `podman run` options (env vars, ports, resources, ...).
|
||||||
PODMAN_ARGS=(
|
PODMAN_ARGS=(
|
||||||
# -e GITHUB_TOKEN
|
# -e GITHUB_TOKEN
|
||||||
|
|||||||
@@ -4,14 +4,19 @@ FROM docker.io/library/debian:stable-slim
|
|||||||
|
|
||||||
# Base tools Claude Code relies on. Add project-wide tools here (python3, nodejs, ...).
|
# Base tools Claude Code relies on. Add project-wide tools here (python3, nodejs, ...).
|
||||||
# The second line is for GUI=1 / AUDIO=1: OpenGL drivers, a font, PulseAudio client.
|
# The second line is for GUI=1 / AUDIO=1: OpenGL drivers, a font, PulseAudio client.
|
||||||
|
# sudo is for ROOT=1: the "sudo" group gets passwordless root. claude is not a
|
||||||
|
# member; claudio.sh adds the group at run time only when ROOT=1.
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends \
|
&& apt-get install -y --no-install-recommends \
|
||||||
ca-certificates curl git ripgrep less procps openssh-client \
|
ca-certificates curl git ripgrep less procps openssh-client \
|
||||||
libgl1-mesa-dri fonts-dejavu-core libpulse0 libasound2-plugins \
|
libgl1-mesa-dri fonts-dejavu-core libpulse0 libasound2-plugins sudo \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& useradd -m -u 1000 -s /bin/bash claude \
|
&& useradd -m -u 1000 -s /bin/bash claude \
|
||||||
&& install -d -m 700 -o claude -g claude /run/user/1000 \
|
&& install -d -m 700 -o claude -g claude /run/user/1000 \
|
||||||
&& printf 'pcm.!default pulse\nctl.!default pulse\n' > /etc/asound.conf
|
&& printf 'pcm.!default pulse\nctl.!default pulse\n' > /etc/asound.conf \
|
||||||
|
&& echo '%sudo ALL=(ALL:ALL) NOPASSWD: ALL' > /etc/sudoers.d/claudio \
|
||||||
|
&& chmod 440 /etc/sudoers.d/claudio \
|
||||||
|
&& echo 'Set disable_coredump false' >> /etc/sudo.conf
|
||||||
|
|
||||||
# Native Claude Code binary, installed system-wide (not in $HOME) so that image
|
# Native Claude Code binary, installed system-wide (not in $HOME) so that image
|
||||||
# rebuilds update it even though /home/claude is a persistent volume.
|
# rebuilds update it even though /home/claude is a persistent volume.
|
||||||
|
|||||||
46
README.md
46
README.md
@@ -7,6 +7,8 @@ Run Claude Code isolated in a podman container, one command per project.
|
|||||||
- `Containerfile`: the image (Debian slim + Claude Code).
|
- `Containerfile`: the image (Debian slim + Claude Code).
|
||||||
- `claudio.sh`: builds, updates and runs the container.
|
- `claudio.sh`: builds, updates and runs the container.
|
||||||
- `.claudio`: template for per-project settings (plain bash).
|
- `.claudio`: template for per-project settings (plain bash).
|
||||||
|
- `test.sh`: end-to-end tests, run on the host (`./test.sh`, or `./test.sh --update`
|
||||||
|
to also test the slow `claudio update`).
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
@@ -27,6 +29,7 @@ claudio --resume # any extra args are passed to claude
|
|||||||
claudio shell # bash inside the container instead of claude
|
claudio shell # bash inside the container instead of claude
|
||||||
claudio build # build the image
|
claudio build # build the image
|
||||||
claudio update # rebuild from scratch: latest base image + latest Claude Code
|
claudio update # rebuild from scratch: latest base image + latest Claude Code
|
||||||
|
claudio reset # remove this project's persistent container (PERSIST=1)
|
||||||
```
|
```
|
||||||
|
|
||||||
The current directory is mounted at the same path inside the container.
|
The current directory is mounted at the same path inside the container.
|
||||||
@@ -43,6 +46,9 @@ cp ~/claudio/.claudio ~/projects/myapp/
|
|||||||
```bash
|
```bash
|
||||||
GUI=1
|
GUI=1
|
||||||
AUDIO=1
|
AUDIO=1
|
||||||
|
ROOT=1
|
||||||
|
PERSIST=1
|
||||||
|
PACKAGES=(python3 nodejs)
|
||||||
MOUNTS=("$HOME/.gitconfig:/home/claude/.gitconfig:ro" "/data/shared")
|
MOUNTS=("$HOME/.gitconfig:/home/claude/.gitconfig:ro" "/data/shared")
|
||||||
PODMAN_ARGS=(-e GITHUB_TOKEN -p 3000:3000)
|
PODMAN_ARGS=(-e GITHUB_TOKEN -p 3000:3000)
|
||||||
CLAUDE_ARGS=(--model opus)
|
CLAUDE_ARGS=(--model opus)
|
||||||
@@ -50,6 +56,9 @@ CLAUDE_ARGS=(--model opus)
|
|||||||
|
|
||||||
- `GUI=1`: graphical apps open on your desktop (see below).
|
- `GUI=1`: graphical apps open on your desktop (see below).
|
||||||
- `AUDIO=1`: apps can play sound (see below).
|
- `AUDIO=1`: apps can play sound (see below).
|
||||||
|
- `ROOT=1`: passwordless `sudo` inside the container (see below).
|
||||||
|
- `PERSIST=1`: keep the container, and system changes, between runs (see below).
|
||||||
|
- `PACKAGES`: extra Debian packages for this project (see below).
|
||||||
- `MOUNTS`: extra `-v` mounts. A bare path is mounted at the same path. Add `:ro` for read-only.
|
- `MOUNTS`: extra `-v` mounts. A bare path is mounted at the same path. Add `:ro` for read-only.
|
||||||
- `PODMAN_ARGS`: extra `podman run` options.
|
- `PODMAN_ARGS`: extra `podman run` options.
|
||||||
- `CLAUDE_ARGS`: default arguments for `claude`.
|
- `CLAUDE_ARGS`: default arguments for `claude`.
|
||||||
@@ -73,11 +82,46 @@ Both are off by default and work by sharing a single socket from your session.
|
|||||||
The image only ships OpenGL drivers, a font and the PulseAudio client. Add the
|
The image only ships OpenGL drivers, a font and the PulseAudio client. Add the
|
||||||
apps you want to run (and their libraries) to the `apt-get install` line.
|
apps you want to run (and their libraries) to the `apt-get install` line.
|
||||||
|
|
||||||
|
## Root access
|
||||||
|
|
||||||
|
`ROOT=1` lets Claude (and you, in `claudio shell`) run `sudo` without a
|
||||||
|
password, for example to `apt-get install` a missing tool. It is off by default:
|
||||||
|
the image grants passwordless sudo to the `sudo` group, and claudio only adds
|
||||||
|
you to that group when `ROOT=1`.
|
||||||
|
|
||||||
|
- System changes last only as long as the container, which is removed after
|
||||||
|
every run unless `PERSIST=1` (see below).
|
||||||
|
- With rootless podman, root in the container is still your unprivileged host
|
||||||
|
user, so it gets no access to the host beyond what is already mounted. With
|
||||||
|
rootful podman it is real root (limited by the container's capabilities).
|
||||||
|
|
||||||
|
## Keeping system changes
|
||||||
|
|
||||||
|
There are two ways, and they combine well:
|
||||||
|
|
||||||
|
- `PACKAGES=(python3 nodejs)` builds a per-project image with those packages
|
||||||
|
on top of the base image (`localhost/claudio:pkgs-<hash>`). It is rebuilt
|
||||||
|
automatically when the list changes or after `claudio update`, so this is
|
||||||
|
the place for anything the project always needs. Projects with the same list
|
||||||
|
share the image.
|
||||||
|
- `PERSIST=1` keeps one container per project instead of a fresh one each run,
|
||||||
|
so anything done with `sudo` (or written outside the mounts) is still there
|
||||||
|
next time. All `claudio` and `claudio shell` sessions in the project share
|
||||||
|
it, and it is stopped when the last one exits. It is **recreated, losing
|
||||||
|
those changes**, when the image changes (`claudio update`, a new
|
||||||
|
`PACKAGES` list) or when the container's options change (`MOUNTS`, `GUI`,
|
||||||
|
`AUDIO`, `ROOT`, `PODMAN_ARGS`); editing `CLAUDE_ARGS` or comments does not. Run `claudio reset`
|
||||||
|
to start it over by hand. Background processes do not survive the stop, and
|
||||||
|
`-e VAR` values in `PODMAN_ARGS` are read when the container is created.
|
||||||
|
|
||||||
|
A good habit: let Claude install things ad hoc with `PERSIST=1`, and move the
|
||||||
|
ones worth keeping into `PACKAGES`.
|
||||||
|
|
||||||
## State
|
## State
|
||||||
|
|
||||||
The container's home (`/home/claude`) lives in the podman volume `claudio-home`.
|
The container's home (`/home/claude`) lives in the podman volume `claudio-home`.
|
||||||
This volume is shared by all projects and holds your login, settings and history.
|
This volume is shared by all projects and holds your login, settings and history.
|
||||||
The container itself is removed after every run.
|
The container itself is removed after every run, unless `PERSIST=1`.
|
||||||
|
|
||||||
- Reset everything: `podman volume rm claudio-home`
|
- Reset everything: `podman volume rm claudio-home`
|
||||||
- Need more tools in the image? Add them to the `apt-get install` line, then run `claudio update`.
|
- Need more tools in the image? Add them to the `apt-get install` line, then run `claudio update`.
|
||||||
|
|||||||
98
claudio.sh
98
claudio.sh
@@ -8,10 +8,14 @@
|
|||||||
# claudio shell [bash args] open a bash shell in the container instead
|
# claudio shell [bash args] open a bash shell in the container instead
|
||||||
# claudio build build the image
|
# claudio build build the image
|
||||||
# claudio update rebuild from scratch (latest Debian + Claude Code)
|
# claudio update rebuild from scratch (latest Debian + Claude Code)
|
||||||
|
# claudio reset remove this project's container (PERSIST=1)
|
||||||
#
|
#
|
||||||
# The current directory is mounted at its own path. If it contains a .claudio
|
# The current directory is mounted at its own path. If it contains a .claudio
|
||||||
# file, that file is sourced (bash) for extra mounts and options, and can set
|
# file, that file is sourced (bash) for extra mounts and options, and can set
|
||||||
# GUI=1 / AUDIO=1 to share the host's Wayland / PulseAudio sockets.
|
# GUI=1 / AUDIO=1 to share the host's Wayland / PulseAudio sockets, ROOT=1
|
||||||
|
# to allow passwordless sudo inside the container, PACKAGES=(...) to build a
|
||||||
|
# per-project image with extra Debian packages, or PERSIST=1 to keep the
|
||||||
|
# container (and any system changes) between runs.
|
||||||
# The container's home (/home/claude: login, settings, history) lives in the
|
# The container's home (/home/claude: login, settings, history) lives in the
|
||||||
# global podman volume "claudio-home".
|
# global podman volume "claudio-home".
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -24,7 +28,10 @@ build() { podman build -t "$IMAGE" "$@" "$DIR"; }
|
|||||||
|
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
build) build; exit ;;
|
build) build; exit ;;
|
||||||
update) build --pull=always --no-cache && podman image prune -f; exit ;;
|
# PACKAGES images built on the old base are stale; they are rebuilt on demand.
|
||||||
|
update) build --pull=always --no-cache \
|
||||||
|
&& podman image prune -af --filter label=claudio.packages \
|
||||||
|
&& podman image prune -f; exit ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [[ "$PWD" == "$HOME" || "$PWD" == / ]]; then
|
if [[ "$PWD" == "$HOME" || "$PWD" == / ]]; then
|
||||||
@@ -32,12 +39,36 @@ if [[ "$PWD" == "$HOME" || "$PWD" == / ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# PERSIST=1 container for this directory: readable name + hash of the full path.
|
||||||
|
name="claudio-$(printf %s "${PWD##*/}" | tr -c 'a-zA-Z0-9_.-' -)-$(printf %s "$PWD" | sha256sum | cut -c1-8)"
|
||||||
|
|
||||||
|
if [[ "${1:-}" == reset ]]; then
|
||||||
|
podman rm -f --ignore "$name" >/dev/null
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
podman image exists "$IMAGE" || build
|
podman image exists "$IMAGE" || build
|
||||||
|
|
||||||
# Project settings (see the .claudio template).
|
# Project settings (see the .claudio template).
|
||||||
MOUNTS=() PODMAN_ARGS=() CLAUDE_ARGS=() GUI=0 AUDIO=0
|
MOUNTS=() PODMAN_ARGS=() CLAUDE_ARGS=() PACKAGES=() GUI=0 AUDIO=0 ROOT=0 PERSIST=0
|
||||||
if [[ -f .claudio ]]; then source ./.claudio; fi
|
if [[ -f .claudio ]]; then source ./.claudio; fi
|
||||||
|
|
||||||
|
# PACKAGES: a derived image, tagged by base image + package list, so changing
|
||||||
|
# either one gives a new tag that gets built on first use.
|
||||||
|
image="$IMAGE"
|
||||||
|
if (( ${#PACKAGES[@]} )); then
|
||||||
|
image="localhost/claudio:pkgs-$(printf '%s\n' "$(podman image inspect -f '{{.Id}}' "$IMAGE")" \
|
||||||
|
"${PACKAGES[@]}" | sha256sum | cut -c1-12)"
|
||||||
|
podman image exists "$image" || podman build -t "$image" --label claudio.packages=1 -f - "$DIR" <<EOF
|
||||||
|
FROM $IMAGE
|
||||||
|
USER root
|
||||||
|
RUN apt-get update \\
|
||||||
|
&& apt-get install -y --no-install-recommends ${PACKAGES[*]@Q} \\
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
USER claude
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
args=(-v "$PWD:$PWD" -w "$PWD" -v "$VOLUME:/home/claude")
|
args=(-v "$PWD:$PWD" -w "$PWD" -v "$VOLUME:/home/claude")
|
||||||
for m in "${MOUNTS[@]}"; do
|
for m in "${MOUNTS[@]}"; do
|
||||||
[[ "$m" == *:* ]] || m="$m:$m" # bare path -> same path inside the container
|
[[ "$m" == *:* ]] || m="$m:$m" # bare path -> same path inside the container
|
||||||
@@ -64,19 +95,54 @@ if [[ "$AUDIO" == 1 ]]; then
|
|||||||
args+=(-e PULSE_SERVER=unix:/run/user/1000/pulse/native)
|
args+=(-e PULSE_SERVER=unix:/run/user/1000/pulse/native)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "${1:-}" == shell ]]; then
|
# ROOT: the image gives the "sudo" group passwordless sudo; join it only on request.
|
||||||
shift
|
if [[ "$ROOT" == 1 ]]; then args+=(--group-add sudo); fi
|
||||||
args+=(--entrypoint bash)
|
|
||||||
CLAUDE_ARGS=()
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -t 0 && -t 1 ]]; then args+=(-it); else args+=(-i); fi
|
|
||||||
|
|
||||||
# keep-id maps your host user to the container's uid 1000, so files written
|
# keep-id maps your host user to the container's uid 1000, so files written
|
||||||
# in mounts stay owned by you. label=disable avoids SELinux relabeling of mounts.
|
# in mounts stay owned by you. label=disable avoids SELinux relabeling of mounts.
|
||||||
exec podman run --rm "${args[@]}" \
|
args+=(--userns=keep-id:uid=1000,gid=1000 --security-opt label=disable --hostname claudio
|
||||||
--userns=keep-id:uid=1000,gid=1000 \
|
"${PODMAN_ARGS[@]}")
|
||||||
--security-opt label=disable \
|
|
||||||
--hostname claudio \
|
cmd=(claude "${CLAUDE_ARGS[@]}")
|
||||||
"${PODMAN_ARGS[@]}" \
|
if [[ "${1:-}" == shell ]]; then
|
||||||
"$IMAGE" "${CLAUDE_ARGS[@]}" "$@"
|
shift
|
||||||
|
cmd=(bash)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -t 0 && -t 1 ]]; then tty=(-it); else tty=(-i); fi
|
||||||
|
|
||||||
|
if [[ "$PERSIST" != 1 ]]; then
|
||||||
|
exec podman run --rm "${tty[@]}" "${args[@]}" --entrypoint "${cmd[0]}" \
|
||||||
|
"$image" "${cmd[@]:1}" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# PERSIST: the container idles under `sleep` and every session is a `podman
|
||||||
|
# exec` into it. It is recreated when the image or the options above change.
|
||||||
|
config="$(printf '%s\n' "$(podman image inspect -f '{{.Id}}' "$image")" "${args[@]}" \
|
||||||
|
| sha256sum | cut -c1-12)"
|
||||||
|
current="$(podman container inspect -f '{{index .Config.Labels "claudio.config"}}' "$name" 2>/dev/null)" \
|
||||||
|
|| current=none
|
||||||
|
if [[ "$current" != "$config" && "$current" != none ]]; then
|
||||||
|
if [[ "$(podman container inspect -f '{{.State.Running}}' "$name")" == true ]]; then
|
||||||
|
echo "claudio: settings or image changed, but $name is in use by another session." >&2
|
||||||
|
echo "claudio: close it (or run 'claudio reset') to recreate the container." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "claudio: settings or image changed, recreating $name (system changes are lost)." >&2
|
||||||
|
podman rm "$name" >/dev/null
|
||||||
|
current=none
|
||||||
|
fi
|
||||||
|
if [[ "$current" == none ]]; then
|
||||||
|
podman create --name "$name" --label claudio.config="$config" --init "${args[@]}" \
|
||||||
|
--entrypoint sleep "$image" infinity >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
podman start "$name" >/dev/null
|
||||||
|
status=0
|
||||||
|
podman exec "${tty[@]}" -w "$PWD" "$name" "${cmd[@]}" "$@" || status=$?
|
||||||
|
|
||||||
|
# Stop once no session is left. Exec'd processes (and the init) have parent
|
||||||
|
# pid 0, so with only the init and this ps left there are no sessions.
|
||||||
|
left="$(podman exec "$name" ps -e -o ppid= | grep -cx ' *0')" || left=0
|
||||||
|
if (( left <= 2 )); then podman stop -t 2 --ignore "$name" >/dev/null; fi
|
||||||
|
exit "$status"
|
||||||
|
|||||||
132
test.sh
Executable file
132
test.sh
Executable file
@@ -0,0 +1,132 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# test.sh — end-to-end tests for claudio, run on the host with the real podman.
|
||||||
|
#
|
||||||
|
# Usage: ./test.sh build the image (cached if unchanged) and test
|
||||||
|
# ./test.sh --update also test `claudio update` (slow: full rebuild)
|
||||||
|
#
|
||||||
|
# It works in throwaway directories under /tmp and removes the containers and
|
||||||
|
# PACKAGES images it creates. The shared claudio-home volume is not touched.
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
CLAUDIO="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/claudio.sh"
|
||||||
|
proj="$(mktemp -d /tmp/claudio-test.XXXXXX)"
|
||||||
|
extra="$(mktemp -d /tmp/claudio-test-mount.XXXXXX)"
|
||||||
|
# Same formula as claudio.sh, to find this project's PERSIST container.
|
||||||
|
name="claudio-$(printf %s "${proj##*/}" | tr -c 'a-zA-Z0-9_.-' -)-$(printf %s "$proj" | sha256sum | cut -c1-8)"
|
||||||
|
pkg_images() { podman images -q --filter label=claudio.packages | sort -u; }
|
||||||
|
pkg_before="$(pkg_images)"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
(cd "$proj" && "$CLAUDIO" reset)
|
||||||
|
comm -13 <(echo "$pkg_before") <(pkg_images) | xargs -r podman rmi -f >/dev/null
|
||||||
|
rm -rf "$proj" "$extra"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
pass=0 fail=0
|
||||||
|
check() { # check "description" command...
|
||||||
|
if "${@:2}"; then echo " ok $1"; ((pass++))
|
||||||
|
else echo " FAIL $1"; ((fail++)); fi
|
||||||
|
}
|
||||||
|
not() { ! "$@"; }
|
||||||
|
cfg() { printf '%s\n' "$@" > "$proj/.claudio"; } # write the project's .claudio
|
||||||
|
claudio() { (cd "$proj" && "$CLAUDIO" "$@" </dev/null); }
|
||||||
|
cin() { claudio shell -c "$1"; } # run a command in the container
|
||||||
|
exits_with() { local want="$1"; shift; "$@"; [[ $? == "$want" ]]; }
|
||||||
|
wait_for() { for _ in {1..30}; do "$@" && return; sleep 1; done; return 1; }
|
||||||
|
running() { [[ "$(podman container inspect -f '{{.State.Running}}' "$name" 2>/dev/null)" == true ]]; }
|
||||||
|
image_of() { podman container inspect -f '{{.ImageName}}' "$name"; }
|
||||||
|
|
||||||
|
from_home() { (cd ~ && "$CLAUDIO" shell -c true </dev/null 2>/dev/null); }
|
||||||
|
owned_by_you() { cin 'touch owned' && [[ "$(stat -c %u "$proj/owned")" == "$(id -u)" ]]; }
|
||||||
|
pkg_image_labeled() { [[ -n "$(podman images -q --filter label=claudio.packages \
|
||||||
|
--filter reference='localhost/claudio:pkgs-*')" ]]; }
|
||||||
|
claude_runs() { claudio --version >/dev/null; }
|
||||||
|
bg_session_up() { podman exec "$name" pgrep -fx 'sleep 8' >/dev/null 2>&1; }
|
||||||
|
|
||||||
|
echo "== build"
|
||||||
|
"$CLAUDIO" build || { echo "build failed"; exit 1; }
|
||||||
|
|
||||||
|
echo "== basics"
|
||||||
|
cfg ''
|
||||||
|
check "refuses to run from \$HOME" not from_home
|
||||||
|
check "runs as uid 1000 in the project dir" cin "[[ \$(id -u) == 1000 && \$PWD == '$proj' ]]"
|
||||||
|
check "claude is installed" claude_runs
|
||||||
|
check "files in the project are yours" owned_by_you
|
||||||
|
check "exit status is passed through" exits_with 3 cin 'exit 3'
|
||||||
|
check "home is the claudio-home volume" cin 'mountpoint -q /home/claude'
|
||||||
|
|
||||||
|
echo "== MOUNTS"
|
||||||
|
cfg "MOUNTS=('$extra')"
|
||||||
|
check "bare path is mounted read-write" cin "touch '$extra/rw'"
|
||||||
|
cfg "MOUNTS=('$extra:/mnt/x:ro')"
|
||||||
|
check "':ro' mount is read-only" cin 'test -f /mnt/x/rw && ! touch /mnt/x/no 2>/dev/null'
|
||||||
|
|
||||||
|
echo "== ROOT"
|
||||||
|
cfg ''
|
||||||
|
check "no sudo without ROOT=1" cin '! sudo -n true 2>/dev/null'
|
||||||
|
cfg 'ROOT=1'
|
||||||
|
check "passwordless sudo with ROOT=1" cin '[[ $(sudo -n id -u) == 0 ]]'
|
||||||
|
check "root can change the system" cin 'sudo -n touch /opt/marker'
|
||||||
|
check "...but it is gone next run" cin '! test -e /opt/marker'
|
||||||
|
|
||||||
|
echo "== PACKAGES"
|
||||||
|
cfg 'PACKAGES=(hello)'
|
||||||
|
check "package is installed" cin 'hello >/dev/null'
|
||||||
|
check "image is labeled claudio.packages" pkg_image_labeled
|
||||||
|
cfg ''
|
||||||
|
check "not installed without PACKAGES" cin '! command -v hello >/dev/null'
|
||||||
|
|
||||||
|
echo "== PERSIST"
|
||||||
|
persist=('ROOT=1' 'PERSIST=1' 'PACKAGES=(hello)')
|
||||||
|
cfg "${persist[@]}"
|
||||||
|
check "system changes are kept" cin 'sudo -n touch /opt/marker'
|
||||||
|
check "...and seen by the next run" cin 'test -e /opt/marker'
|
||||||
|
check "PACKAGES and sudo work too" cin 'hello >/dev/null && sudo -n true'
|
||||||
|
check "stopped after the run" not running
|
||||||
|
check "exit status is passed through" exits_with 3 cin 'exit 3'
|
||||||
|
check "claudio (not shell) runs claude" claude_runs
|
||||||
|
|
||||||
|
cin 'sleep 8' & bg=$!
|
||||||
|
check "background session started" wait_for bg_session_up
|
||||||
|
cin true
|
||||||
|
check "still running while a session is" running
|
||||||
|
cfg "${persist[@]}" "MOUNTS=('$extra')" # must change the podman options, not just the file
|
||||||
|
check "refuses to recreate while in use" not cin true 2>/dev/null
|
||||||
|
cfg "${persist[@]}"
|
||||||
|
wait "$bg"
|
||||||
|
check "stopped after the last session" not running
|
||||||
|
|
||||||
|
cfg "${persist[@]}" "MOUNTS=('$extra')"
|
||||||
|
check "settings change recreates it" cin '! test -e /opt/marker'
|
||||||
|
claudio reset
|
||||||
|
check "reset removes the container" not podman container exists "$name"
|
||||||
|
cfg "${persist[@]}"
|
||||||
|
cin 'sudo -n touch /opt/marker'
|
||||||
|
cfg "${persist[@]/#PERSIST=1/PERSIST=0}"
|
||||||
|
check "PERSIST=0 ignores the container" cin '! test -e /opt/marker'
|
||||||
|
|
||||||
|
echo "== GUI / AUDIO (skipped if your session has no such socket)"
|
||||||
|
host_run="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
|
||||||
|
wl="${WAYLAND_DISPLAY:-wayland-0}"
|
||||||
|
if [[ -S "$host_run/$wl" ]]; then
|
||||||
|
cfg 'GUI=1'
|
||||||
|
check "Wayland socket is shared" cin "test -S /run/user/1000/$wl && [[ \$WAYLAND_DISPLAY == '$wl' ]]"
|
||||||
|
else echo " skip GUI (no $host_run/$wl)"; fi
|
||||||
|
if [[ -S "$host_run/pulse/native" ]]; then
|
||||||
|
cfg 'AUDIO=1'
|
||||||
|
check "PulseAudio socket is shared" cin 'test -S /run/user/1000/pulse/native'
|
||||||
|
else echo " skip AUDIO (no $host_run/pulse/native)"; fi
|
||||||
|
|
||||||
|
if [[ "${1:-}" == --update ]]; then
|
||||||
|
echo "== update"
|
||||||
|
cfg "${persist[@]}"
|
||||||
|
cin 'sudo -n touch /opt/marker'
|
||||||
|
old="$(image_of)"
|
||||||
|
"$CLAUDIO" update
|
||||||
|
check "update recreates the container" cin '! test -e /opt/marker && hello >/dev/null'
|
||||||
|
check "...on a new PACKAGES image" [ "$(image_of)" != "$old" ]
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "== $pass passed, $fail failed"
|
||||||
|
(( fail == 0 ))
|
||||||
Reference in New Issue
Block a user