Files
OpenShell/scripts/docker-cleanup.sh
T
Adam Miller d44d8a1e27 feat: Openshell driver podman (#904)
* feat(podman): add Podman compute driver for rootless sandbox management

Adds openshell-driver-podman, a new compute driver that manages OpenShell
sandboxes as rootless Podman containers via the Podman REST API over a
Unix socket. Enables local workstation sandboxes without Kubernetes.

Driver features:
- Bridge networking with ephemeral host-port mapping for rootless SSH reachability
- Named volumes for workspace storage, Podman native health checks, GPU via CDI
- Supervisor binary sideloaded via image volume mount (BYOC-compatible)
- SSH handshake secret injected via Podman secrets API (not plaintext env)
- Typed ContainerSpec structs, input validation, and path-traversal guards
- Cgroups v2 required; fails fast on v1 hosts
- Bounded event stream buffer; watch stream reconnection handled by server watch_loop
- Graceful shutdown and standalone driver binary with gRPC bridge

Rootless-specific fixes:
- Skip drop_privileges when user namespace lacks SETUID/SETGID/DAC_READ_SEARCH caps
- Add /run/netns tmpfs mount for ip netns in rootless containers
- Use secret_env map (not secrets array) for env-var injection in libpod API
- Resolve SSH endpoint to 127.0.0.1:<host_port> instead of unreachable bridge IP

Server/sandbox hardening:
- Split loopback and link-local SSRF gates; Podman/VM drivers allow loopback
- Close SSRF bypass in SSH tunnel Host path by resolving DNS before connecting
- Prevent OPENSHELL_* env var override by user-supplied spec environment maps
- Disable SQLite pool idle_timeout/max_lifetime for in-memory databases
- Emit deleted_event on 404-during-inspect instead of regressing sandbox phase
- Key delete cleanup by stable sandbox_id to survive container label drift

CLI fixes:
- Restore --name as a named flag on sandbox create (not positional)
- Fix exec command arg parsing to not consume sandboxed-command flags
- Propagate SSH verbosity via OPENSHELL_SSH_LOG_LEVEL

Build tooling:
- Add tasks/scripts/container-engine.sh: auto-detects Podman or Docker, exposes
  unified ce_* helpers; all build/cluster/VM scripts updated to use it
- Add docker:build:supervisor mise task for standalone supervisor image
- Add openshell-driver-podman to Dockerfile.images pre-fetch/build stages
- Add e2e/rust/e2e-podman.sh and e2e:podman mise task for full lifecycle testing

Signed-off-by: Adam Miller <admiller@redhat.com>

* fix(driver-podman): derive grpc endpoint from server bind port

When a user starts the gateway on a non-default port (e.g. --port 8081),
sandbox containers were receiving OPENSHELL_ENDPOINT pointing at the
default port 8080. The driver's auto-detection fallback read
OPENSHELL_BIND_ADDRESS from the environment, which was stale or unset,
and fell back to DEFAULT_SERVER_PORT.

Add gateway_port to PodmanComputeConfig and thread config.bind_address.port()
from the server into the driver so the fallback uses the actual listening
port. Remove the OPENSHELL_BIND_ADDRESS env var read and the
extract_port_from_bind_address helper which are no longer needed.

Add --gateway-port / OPENSHELL_GATEWAY_PORT to the standalone driver
binary for parity when the driver is run outside the embedded server path.

Signed-off-by: Adam Miller <admiller@redhat.com>

* fix(driver-podman): address PR feedback on env test safety and cluster DNS docs

Replace hand-rolled unsafe TempEnvVar RAII guard with temp_env::with_vars
and a static ENV_LOCK mutex, fixing a data race in parallel test execution.
The prior safety comment incorrectly claimed Cargo runs tests single-threaded.

Update debug-openshell-cluster skill to accurately document the DNS proxy
strategy (setup_dns_proxy + public DNS fallback) and clarify the separation
between cluster DNS and sandbox agent DNS enforcement.

Signed-off-by: Adam Miller <admiller@redhat.com>

* fix(e2e): resolve CI failures in auth timeout, test harness, and formatting

- Short-circuit browser_auth_flow when OPENSHELL_NO_BROWSER=1 instead
  of waiting the full 120s AUTH_TIMEOUT for a callback that never arrives
- Add timeout to SandboxGuard::create() and create_with_upload() to
  prevent indefinite hangs (matches create_keep() which already had one)
- Add missing '--' separator in no_proxy test before command args
- Add #![cfg(feature = "e2e")] gate to sandbox_lifecycle.rs
- Run cargo fmt on openshell-driver-podman
- Refine cluster DNS docs for Podman in debug-openshell-cluster skill

Signed-off-by: Adam Miller <admiller@redhat.com>

* refactor(server): remove allows_loopback_endpoints from ComputeRuntime

SSRF protection is now handled at the network and proxy layers
(openshell-core net.rs, openshell-sandbox proxy.rs) rather than
requiring per-driver flags on ComputeRuntime. Update architecture
docs to reflect supervisor relay SSH transport and add rootless
networking deep-dive.

Signed-off-by: Adam Miller <admiller@redhat.com>

---------

Signed-off-by: Adam Miller <admiller@redhat.com>
2026-04-24 10:30:14 -07:00

288 lines
9.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
# Clean up stale Docker images, volumes, and build cache that are not in use
# by the currently deployed OpenShell cluster.
#
# Preserves:
# - Current openshell/* :dev images (and 127.0.0.1:5000 registry mirrors)
# - Images actively used by running containers
# - Common base/infrastructure images (registry, k3s, alpine, rust, python, etc.)
# - Volumes attached to running containers
#
# Usage:
# ./scripts/docker-cleanup.sh [options]
#
# Options:
# --dry-run Show what would be removed without deleting anything
# --force Skip the confirmation prompt
# --skip-cache Skip build cache pruning (cache speeds up subsequent builds)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../tasks/scripts/container-engine.sh"
# ---------------------------------------------------------------------------
# Options
# ---------------------------------------------------------------------------
DRY_RUN=false
SKIP_CACHE=false
FORCE=false
usage() {
sed -n '/^# Usage:/,/^$/p' "$0" | sed 's/^# \?//'
exit 0
}
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
--skip-cache) SKIP_CACHE=true ;;
--force) FORCE=true ;;
-h|--help) usage ;;
*) echo "Unknown option: $arg"; usage ;;
esac
done
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
BOLD='\033[1m'
DIM='\033[2m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
RED='\033[0;31m'
RESET='\033[0m'
info() { echo -e "${BOLD}==> $*${RESET}"; }
note() { echo -e " ${DIM}$*${RESET}"; }
ok() { echo -e " ${GREEN}$*${RESET}"; }
warn() { echo -e " ${YELLOW}$*${RESET}"; }
err() { echo -e " ${RED}$*${RESET}"; }
dry() { echo -e " ${YELLOW}[dry-run]${RESET} $*"; }
# ---------------------------------------------------------------------------
# Preflight
# ---------------------------------------------------------------------------
# container-engine.sh already validates that an engine is available.
if ! ce info &>/dev/null; then
echo "Error: Container engine (${CONTAINER_ENGINE}) is not running" >&2
exit 1
fi
# ---------------------------------------------------------------------------
# Image repository prefixes that belong to the CURRENT cluster deployment.
# Any tagged image whose repository starts with one of these is kept.
# ---------------------------------------------------------------------------
CURRENT_IMAGE_PREFIXES=(
"openshell/"
"127.0.0.1:5000/openshell/"
)
# ---------------------------------------------------------------------------
# Infrastructure / base images to always keep (matched by repository prefix).
# These are pulled from remote registries and are expensive to re-download.
# ---------------------------------------------------------------------------
KEEP_IMAGE_PREFIXES=(
"registry"
"rancher/k3s"
"ghcr.io/k3d-io/"
"kindest/"
"alpine"
"python"
"rust"
"ghcr.io/astral-sh/uv"
"redis"
"pgvector/"
"minio/"
)
# Returns 0 if the image repository should be kept, 1 otherwise.
should_keep_image() {
local repo="$1"
# Keep current cluster images
for prefix in "${CURRENT_IMAGE_PREFIXES[@]}"; do
if [[ "$repo" == "$prefix"* ]]; then
return 0
fi
done
# Keep infrastructure / base images
for prefix in "${KEEP_IMAGE_PREFIXES[@]}"; do
if [[ "$repo" == "$prefix"* ]]; then
return 0
fi
done
return 1
}
# ---------------------------------------------------------------------------
# Snapshot disk usage before cleanup
# ---------------------------------------------------------------------------
info "Current disk usage (${CONTAINER_ENGINE})"
ce system df
echo
# ---------------------------------------------------------------------------
# Confirmation
# ---------------------------------------------------------------------------
if [[ "$DRY_RUN" == true ]]; then
warn "Running in dry-run mode -- nothing will be deleted"
echo
elif [[ "$FORCE" != true ]]; then
echo -e "${BOLD}This will remove stale images, unused volumes, and build cache.${RESET}"
echo "The currently deployed cluster images and running containers are preserved."
echo
read -r -p "Continue? [y/N] " confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 0
fi
echo
fi
# Track totals for the summary
TOTAL_IMAGES_REMOVED=0
TOTAL_VOLUMES_REMOVED=0
# ---------------------------------------------------------------------------
# Step 1: Remove dangling (untagged) images
# ---------------------------------------------------------------------------
info "Removing dangling images..."
dangling_count=$(ce images --filter "dangling=true" -q | wc -l | tr -d ' ')
if [[ "$dangling_count" -gt 0 ]]; then
note "Found $dangling_count dangling image(s)"
if [[ "$DRY_RUN" == true ]]; then
dry "Would remove $dangling_count dangling image(s)"
else
ce image prune -f | tail -1
TOTAL_IMAGES_REMOVED=$((TOTAL_IMAGES_REMOVED + dangling_count))
fi
else
ok "No dangling images found"
fi
echo
# ---------------------------------------------------------------------------
# Step 2: Remove stale tagged images
# ---------------------------------------------------------------------------
info "Removing stale tagged images..."
# Collect image IDs that are directly used by running containers so we never
# touch them regardless of tag matching.
running_image_ids=$(ce ps -q 2>/dev/null | xargs -r ce inspect --format '{{.Image}}' 2>/dev/null | sort -u)
stale_images=()
while IFS=$'\t' read -r repo tag id; do
# Skip dangling images (already handled above)
[[ "$repo" == "<none>" ]] && continue
# Never remove images actively used by a container
if echo "$running_image_ids" | grep -q "$id" 2>/dev/null; then
continue
fi
if ! should_keep_image "$repo"; then
stale_images+=("${repo}:${tag}")
fi
done < <(ce images --format '{{.Repository}}\t{{.Tag}}\t{{.ID}}')
if [[ ${#stale_images[@]} -gt 0 ]]; then
for img in "${stale_images[@]}"; do
if [[ "$DRY_RUN" == true ]]; then
dry "Would remove $img"
else
note "Removing $img"
ce rmi "$img" >/dev/null 2>&1 || warn "Could not remove $img (may share layers)"
TOTAL_IMAGES_REMOVED=$((TOTAL_IMAGES_REMOVED + 1))
fi
done
else
ok "No stale tagged images found"
fi
echo
# ---------------------------------------------------------------------------
# Step 3: Remove unused volumes
# ---------------------------------------------------------------------------
info "Removing unused volumes..."
# Identify volumes in use by running containers
in_use_volumes=$(ce ps -q 2>/dev/null \
| xargs -r ce inspect --format '{{range .Mounts}}{{.Name}} {{end}}' 2>/dev/null \
| tr ' ' '\n' | sort -u | grep -v '^$')
unused_volumes=()
while read -r vol; do
[[ -z "$vol" ]] && continue
if ! echo "$in_use_volumes" | grep -qx "$vol" 2>/dev/null; then
unused_volumes+=("$vol")
fi
done < <(ce volume ls -q)
if [[ ${#unused_volumes[@]} -gt 0 ]]; then
note "Found ${#unused_volumes[@]} unused volume(s)"
for vol in "${unused_volumes[@]}"; do
if [[ "$DRY_RUN" == true ]]; then
dry "Would remove volume $vol"
else
ce volume rm "$vol" >/dev/null 2>&1 || warn "Could not remove volume $vol"
TOTAL_VOLUMES_REMOVED=$((TOTAL_VOLUMES_REMOVED + 1))
fi
done
else
ok "No unused volumes found"
fi
echo
# ---------------------------------------------------------------------------
# Step 4: Prune build cache
# ---------------------------------------------------------------------------
if [[ "$SKIP_CACHE" == true ]]; then
info "Skipping build cache prune (--skip-cache)"
else
info "Pruning build cache..."
cache_size=$(ce system df --format '{{.Size}}' 2>/dev/null | tail -1)
if [[ "$DRY_RUN" == true ]]; then
dry "Would prune build cache (current size: ${cache_size:-unknown})"
else
ce_builder_prune -af 2>&1 | tail -1
fi
fi
echo
# ---------------------------------------------------------------------------
# Step 5: Clean up any newly-dangling images left after tagged image removal
# ---------------------------------------------------------------------------
remaining_dangling=$(ce images --filter "dangling=true" -q | wc -l | tr -d ' ')
if [[ "$remaining_dangling" -gt 0 ]]; then
info "Cleaning up $remaining_dangling residual dangling image(s)..."
if [[ "$DRY_RUN" != true ]]; then
ce image prune -f >/dev/null 2>&1
TOTAL_IMAGES_REMOVED=$((TOTAL_IMAGES_REMOVED + remaining_dangling))
fi
echo
fi
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
info "Cleanup complete"
echo
if [[ "$DRY_RUN" == true ]]; then
warn "Dry run -- no changes were made. Re-run without --dry-run to apply."
else
ce system df
fi
echo
info "Remaining images:"
ce images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"