Files
OpenShell/tasks/scripts/docker-publish-multiarch.sh
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

155 lines
5.4 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
# Build multi-arch gateway + cluster images and push to a container registry.
# Requires DOCKER_REGISTRY to be set (e.g. ghcr.io/myorg).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/container-engine.sh"
REGISTRY=${DOCKER_REGISTRY:?Set DOCKER_REGISTRY to push multi-arch images (e.g. ghcr.io/myorg)}
IMAGE_TAG=${IMAGE_TAG:-dev}
PLATFORMS=${DOCKER_PLATFORMS:-linux/amd64,linux/arm64}
TAG_LATEST=${TAG_LATEST:-false}
EXTRA_DOCKER_TAGS_RAW=${EXTRA_DOCKER_TAGS:-}
EXTRA_TAGS=()
if [[ -n "${EXTRA_DOCKER_TAGS_RAW}" ]]; then
EXTRA_DOCKER_TAGS_RAW=${EXTRA_DOCKER_TAGS_RAW//,/ }
for tag in ${EXTRA_DOCKER_TAGS_RAW}; do
[[ -n "${tag}" ]] && EXTRA_TAGS+=("${tag}")
done
fi
# ---------------------------------------------------------------------------
# Docker path: use buildx builders + imagetools for multi-arch
# ---------------------------------------------------------------------------
_publish_multiarch_docker() {
BUILDER_NAME=${DOCKER_BUILDER:-multiarch}
if ce buildx inspect "${BUILDER_NAME}" >/dev/null 2>&1; then
echo "Using existing buildx builder: ${BUILDER_NAME}"
ce buildx use "${BUILDER_NAME}"
else
echo "Creating multi-platform buildx builder: ${BUILDER_NAME}..."
ce buildx create --name "${BUILDER_NAME}" --use --bootstrap
fi
export DOCKER_BUILDER="${BUILDER_NAME}"
export DOCKER_PLATFORM="${PLATFORMS}"
export DOCKER_PUSH=1
export IMAGE_REGISTRY="${REGISTRY}"
echo "Building multi-arch gateway image..."
tasks/scripts/docker-build-image.sh gateway
echo
echo "Building multi-arch cluster image..."
tasks/scripts/docker-build-image.sh cluster
TAGS_TO_APPLY=("${EXTRA_TAGS[@]}")
if [[ "${TAG_LATEST}" == "true" ]]; then
TAGS_TO_APPLY+=("latest")
fi
if [[ ${#TAGS_TO_APPLY[@]} -gt 0 ]]; then
for component in gateway cluster; do
full_image="${REGISTRY}/${component}"
for tag in "${TAGS_TO_APPLY[@]}"; do
[[ "${tag}" == "${IMAGE_TAG}" ]] && continue
echo "Tagging ${full_image}:${tag}..."
ce_imagetools_create \
--prefer-index=false \
-t "${full_image}:${tag}" \
"${full_image}:${IMAGE_TAG}"
done
done
fi
}
# ---------------------------------------------------------------------------
# Podman path: build per-platform, assemble manifest lists, push
# ---------------------------------------------------------------------------
_publish_multiarch_podman() {
export IMAGE_REGISTRY="${REGISTRY}"
# Split comma-separated platforms into an array.
IFS=',' read -ra PLATFORM_LIST <<< "${PLATFORMS}"
for component in gateway cluster; do
local full_image="${REGISTRY}/${component}"
local manifest_name="${full_image}:${IMAGE_TAG}"
# Remove any pre-existing manifest list.
ce manifest rm "${manifest_name}" 2>/dev/null || true
ce manifest create "${manifest_name}"
echo "Building multi-arch ${component} image..."
for platform in "${PLATFORM_LIST[@]}"; do
echo " Building ${component} for ${platform}..."
# Build for each platform and add to the manifest list.
# docker-build-image.sh sources container-engine.sh itself,
# so ce_build is used internally.
DOCKER_PLATFORM="${platform}" \
DOCKER_PUSH="" \
IMAGE_TAG="${IMAGE_TAG}" \
tasks/scripts/docker-build-image.sh "${component}"
# Tag with a platform-specific suffix for manifest assembly.
local platform_tag="${IMAGE_TAG}-${platform//\//-}"
ce tag "openshell/${component}:${IMAGE_TAG}" "${full_image}:${platform_tag}"
ce push "${full_image}:${platform_tag}"
ce manifest add "${manifest_name}" "${full_image}:${platform_tag}"
done
echo "Pushing manifest ${manifest_name}..."
ce manifest push --all "${manifest_name}" "docker://${manifest_name}"
# Apply extra tags.
TAGS_TO_APPLY=("${EXTRA_TAGS[@]}")
if [[ "${TAG_LATEST}" == "true" ]]; then
TAGS_TO_APPLY+=("latest")
fi
for tag in "${TAGS_TO_APPLY[@]}"; do
[[ "${tag}" == "${IMAGE_TAG}" ]] && continue
echo "Tagging ${full_image}:${tag}..."
ce manifest create "${full_image}:${tag}" 2>/dev/null || ce manifest rm "${full_image}:${tag}" 2>/dev/null
# Re-create from the primary manifest.
if command -v skopeo >/dev/null 2>&1; then
skopeo copy --all "docker://${manifest_name}" "docker://${full_image}:${tag}"
else
ce manifest create "${full_image}:${tag}"
for platform in "${PLATFORM_LIST[@]}"; do
local platform_tag="${IMAGE_TAG}-${platform//\//-}"
ce manifest add "${full_image}:${tag}" "${full_image}:${platform_tag}"
done
ce manifest push --all "${full_image}:${tag}" "docker://${full_image}:${tag}"
fi
done
done
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
if ce_is_docker; then
_publish_multiarch_docker
else
_publish_multiarch_podman
fi
echo
echo "Done! Multi-arch images pushed to ${REGISTRY}:"
echo " ${REGISTRY}/gateway:${IMAGE_TAG}"
echo " ${REGISTRY}/cluster:${IMAGE_TAG}"
if [[ "${TAG_LATEST}" == "true" ]]; then
echo " (all also tagged :latest)"
fi
if [[ ${#EXTRA_TAGS[@]} -gt 0 ]]; then
echo " (all also tagged: ${EXTRA_TAGS[*]})"
fi