Add opt-in root access, persistent containers and per-project packages
This commit is contained in:
98
claudio.sh
98
claudio.sh
@@ -8,10 +8,14 @@
|
||||
# claudio shell [bash args] open a bash shell in the container instead
|
||||
# claudio build build the image
|
||||
# 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
|
||||
# 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
|
||||
# global podman volume "claudio-home".
|
||||
set -euo pipefail
|
||||
@@ -24,7 +28,10 @@ build() { podman build -t "$IMAGE" "$@" "$DIR"; }
|
||||
|
||||
case "${1:-}" in
|
||||
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
|
||||
|
||||
if [[ "$PWD" == "$HOME" || "$PWD" == / ]]; then
|
||||
@@ -32,12 +39,36 @@ if [[ "$PWD" == "$HOME" || "$PWD" == / ]]; then
|
||||
exit 1
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
# 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")
|
||||
for m in "${MOUNTS[@]}"; do
|
||||
[[ "$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)
|
||||
fi
|
||||
|
||||
if [[ "${1:-}" == shell ]]; then
|
||||
shift
|
||||
args+=(--entrypoint bash)
|
||||
CLAUDE_ARGS=()
|
||||
fi
|
||||
|
||||
if [[ -t 0 && -t 1 ]]; then args+=(-it); else args+=(-i); fi
|
||||
# ROOT: the image gives the "sudo" group passwordless sudo; join it only on request.
|
||||
if [[ "$ROOT" == 1 ]]; then args+=(--group-add sudo); fi
|
||||
|
||||
# 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.
|
||||
exec podman run --rm "${args[@]}" \
|
||||
--userns=keep-id:uid=1000,gid=1000 \
|
||||
--security-opt label=disable \
|
||||
--hostname claudio \
|
||||
"${PODMAN_ARGS[@]}" \
|
||||
"$IMAGE" "${CLAUDE_ARGS[@]}" "$@"
|
||||
args+=(--userns=keep-id:uid=1000,gid=1000 --security-opt label=disable --hostname claudio
|
||||
"${PODMAN_ARGS[@]}")
|
||||
|
||||
cmd=(claude "${CLAUDE_ARGS[@]}")
|
||||
if [[ "${1:-}" == shell ]]; then
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user