mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-23 06:30:28 +08:00
* 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>
288 lines
9.0 KiB
Bash
Executable File
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}}"
|