Containerize local builds with rootless nerdctl
This commit is contained in:
parent
4aef12054e
commit
30fc02d79f
6 changed files with 216 additions and 31 deletions
45
README.md
Normal file
45
README.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Lazaret
|
||||
|
||||
Rootless gVisor sandbox for development tools and coding agents.
|
||||
|
||||
## Status
|
||||
|
||||
- Early development
|
||||
- Source only; no binary releases
|
||||
- Linux x86-64, kernel 6.9 or newer
|
||||
|
||||
## Host requirements
|
||||
|
||||
- Unprivileged user namespaces
|
||||
- Contiguous subordinate UID and GID ranges of at least 65,536 IDs
|
||||
- `/usr/bin/newuidmap`, `/usr/bin/newgidmap`, and `/usr/bin/slirp4netns`
|
||||
- `/dev/fuse`, `fuse-overlayfs`, and `fusermount3`
|
||||
- Rootless nerdctl with BuildKit
|
||||
- `cmp`, `install`, and `sha256sum`
|
||||
|
||||
## Build and install
|
||||
|
||||
```sh
|
||||
tools/install-to-host
|
||||
```
|
||||
|
||||
The default destination is `~/.local/bin`. Pass `--install-dir DIRECTORY` to override it.
|
||||
|
||||
## Configure and run
|
||||
|
||||
```sh
|
||||
mkdir -p ~/.config/lazaret
|
||||
cp docs/example.toml ~/.config/lazaret/config.toml
|
||||
lazaret default
|
||||
lazaret default -- COMMAND ARGUMENT...
|
||||
```
|
||||
|
||||
Edit the example network allowlists and persistent volume paths before use.
|
||||
|
||||
## Repository
|
||||
|
||||
- [Product specification](docs/spec/lazaret.md)
|
||||
- [Example configuration](docs/example.toml)
|
||||
- [Verification entry point](tools/verify)
|
||||
- [Apache License 2.0](LICENSE)
|
||||
- [Third-party notices](THIRD_PARTY_NOTICES.md)
|
||||
|
|
@ -448,6 +448,27 @@ class VerificationTests(unittest.TestCase):
|
|||
(ROOT / "patches/gvisor-9f8f6c917-MODULE.bazel.lock").is_file()
|
||||
)
|
||||
|
||||
def test_canonical_build_uses_one_unprivileged_nerdctl_container(self) -> None:
|
||||
build = (ROOT / "tools/build").read_text()
|
||||
containerfile = (ROOT / "tools/container/Containerfile").read_text()
|
||||
installer = (ROOT / "tools/install-to-host").read_text()
|
||||
materializer = (ROOT / "tools/materialize-runsc").read_text()
|
||||
|
||||
self.assertIn("nerdctl build", build)
|
||||
self.assertIn("nerdctl run --rm", build)
|
||||
self.assertIn("tools/materialize-runsc --build", build)
|
||||
self.assertIn("cargo build", build)
|
||||
self.assertIn("GIT_CEILING_DIRECTORIES=/workspace", build)
|
||||
self.assertNotIn("--privileged", build)
|
||||
self.assertNotIn("docker.sock", build)
|
||||
self.assertIn("rust:1.97.0-bookworm@sha256:", containerfile)
|
||||
self.assertIn("ARG BAZEL_VERSION=8.3.1", containerfile)
|
||||
self.assertIn("tools/build", installer)
|
||||
self.assertNotIn("cargo build", installer)
|
||||
self.assertNotIn("readelf", installer)
|
||||
self.assertIn("bazel build", materializer)
|
||||
self.assertNotIn("make -C", materializer)
|
||||
|
||||
def test_workspace_tests_use_nextest_and_cargo_retains_doctests(self) -> None:
|
||||
source = (ROOT / "tools/lazaret_verify.py").read_text()
|
||||
expected = [
|
||||
|
|
|
|||
67
tools/build
Executable file
67
tools/build
Executable file
|
|
@ -0,0 +1,67 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
containerfile="$root/tools/container/Containerfile"
|
||||
|
||||
fail() {
|
||||
printf 'build: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == --inside-container && $# -eq 1 ]]; then
|
||||
[[ "${LAZARET_BUILD_CONTAINER:-}" == 1 ]] || fail 'container marker is absent'
|
||||
cd "$root"
|
||||
tools/materialize-fuser
|
||||
tools/materialize-runsc --build
|
||||
cargo build \
|
||||
--release \
|
||||
--locked \
|
||||
--package lazaret-client \
|
||||
--package lazaret-host-coordinator \
|
||||
--package lazaret-escalation-client \
|
||||
--bins
|
||||
env RUSTFLAGS="-C relocation-model=static -C link-arg=-no-pie" cargo build \
|
||||
--release \
|
||||
--locked \
|
||||
--target x86_64-unknown-linux-musl \
|
||||
--package lazaret-sandbox-init \
|
||||
--bin lazaret-sandbox-init
|
||||
sandbox_init="$CARGO_TARGET_DIR/x86_64-unknown-linux-musl/release/lazaret-sandbox-init"
|
||||
if readelf -l "$sandbox_init" | grep -q 'Requesting program interpreter' ||
|
||||
! readelf -d "$sandbox_init" 2>&1 | grep -q 'There is no dynamic section'; then
|
||||
fail 'sandbox init is dynamically linked'
|
||||
fi
|
||||
exit 0
|
||||
elif [[ $# -ne 0 ]]; then
|
||||
printf '%s\n' 'usage: tools/build' >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
for command in nerdctl sha256sum; do
|
||||
command -v "$command" >/dev/null 2>&1 || fail "$command is required"
|
||||
done
|
||||
|
||||
mkdir -p "$root/.lazaret"
|
||||
containerfile_digest="$(sha256sum "$containerfile")"
|
||||
containerfile_digest="${containerfile_digest%% *}"
|
||||
image="localhost/lazaret-builder:${containerfile_digest:0:16}"
|
||||
proxy_options=()
|
||||
for variable in ALL_PROXY HTTP_PROXY HTTPS_PROXY NO_PROXY all_proxy http_proxy https_proxy no_proxy; do
|
||||
if [[ -v $variable ]]; then
|
||||
proxy_options+=(--env "$variable")
|
||||
fi
|
||||
done
|
||||
nerdctl build --tag "$image" --file "$containerfile" "$root/tools/container"
|
||||
nerdctl run --rm \
|
||||
--mount "type=bind,src=$root,dst=/workspace,ro" \
|
||||
--mount "type=bind,src=$root/.lazaret,dst=/workspace/.lazaret" \
|
||||
--workdir /workspace \
|
||||
--env CARGO_HOME=/workspace/.lazaret/cache/cargo \
|
||||
--env CARGO_TARGET_DIR=/workspace/.lazaret/target \
|
||||
--env GIT_CEILING_DIRECTORIES=/workspace \
|
||||
--env HOME=/workspace/.lazaret/home \
|
||||
--env LAZARET_BUILD_CONTAINER=1 \
|
||||
"${proxy_options[@]}" \
|
||||
"$image" \
|
||||
tools/build --inside-container
|
||||
49
tools/container/Containerfile
Normal file
49
tools/container/Containerfile
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
FROM docker.io/library/rust:1.97.0-bookworm@sha256:8fa55b2f3ddf97471ab6a767bfa3f37e6bad0986ba823e75fea57e2a2a5c3073
|
||||
|
||||
ARG BAZEL_VERSION=8.3.1
|
||||
ARG BAZEL_SHA256=17247e8a84245f59d3bc633d0cfe0a840992a7760a11af1a30012d03da31604c
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
binutils \
|
||||
build-essential \
|
||||
busybox-static \
|
||||
ca-certificates \
|
||||
clang \
|
||||
cmake \
|
||||
crossbuild-essential-amd64 \
|
||||
crossbuild-essential-arm64 \
|
||||
curl \
|
||||
diffutils \
|
||||
erofs-utils \
|
||||
git \
|
||||
iproute2 \
|
||||
iptables \
|
||||
kmod \
|
||||
libbpf-dev \
|
||||
libc6-dev-i386 \
|
||||
libffi-dev \
|
||||
libnuma-dev \
|
||||
libssl-dev \
|
||||
musl-tools \
|
||||
netcat-openbsd \
|
||||
openjdk-17-jdk-headless \
|
||||
patch \
|
||||
pkg-config \
|
||||
python-is-python3 \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-setuptools \
|
||||
qemu-user-static \
|
||||
unzip \
|
||||
zip \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl --fail --location \
|
||||
"https://releases.bazel.build/${BAZEL_VERSION}/release/bazel-${BAZEL_VERSION}-linux-x86_64" \
|
||||
--output /usr/local/bin/bazel \
|
||||
&& printf '%s %s\n' "$BAZEL_SHA256" /usr/local/bin/bazel | sha256sum --check - \
|
||||
&& chmod 0755 /usr/local/bin/bazel \
|
||||
&& rustup target add x86_64-unknown-linux-musl \
|
||||
&& rustup component add clippy rustfmt
|
||||
|
||||
WORKDIR /workspace
|
||||
|
|
@ -18,7 +18,7 @@ elif [ "$#" -ne 0 ]; then
|
|||
exit 2
|
||||
fi
|
||||
|
||||
for command in cargo cmp install readelf; do
|
||||
for command in cmp install; do
|
||||
command -v "$command" >/dev/null 2>&1 || {
|
||||
printf '%s\n' "install-to-host: $command is required" >&2
|
||||
exit 1
|
||||
|
|
@ -26,37 +26,15 @@ for command in cargo cmp install readelf; do
|
|||
done
|
||||
|
||||
cd "$root"
|
||||
tools/materialize-fuser
|
||||
tools/materialize-runsc --build
|
||||
cargo build \
|
||||
--release \
|
||||
--locked \
|
||||
--package lazaret-client \
|
||||
--package lazaret-host-coordinator \
|
||||
--package lazaret-escalation-client \
|
||||
--bins
|
||||
env RUSTFLAGS="-C relocation-model=static -C link-arg=-no-pie" cargo build \
|
||||
--release \
|
||||
--locked \
|
||||
--target x86_64-unknown-linux-musl \
|
||||
--package lazaret-sandbox-init \
|
||||
--bin lazaret-sandbox-init
|
||||
tools/build
|
||||
|
||||
target_dir="${CARGO_TARGET_DIR:-target}"
|
||||
if [[ "$target_dir" != /* ]]; then
|
||||
target_dir="$root/$target_dir"
|
||||
fi
|
||||
target_dir="$root/.lazaret/target"
|
||||
|
||||
install -d -m 755 "$install_dir"
|
||||
for executable in lazaret lazaret-host-coordinator lazaret-escalation-client; do
|
||||
install -m 755 "$target_dir/release/$executable" "$install_dir/$executable"
|
||||
done
|
||||
sandbox_init="$target_dir/x86_64-unknown-linux-musl/release/lazaret-sandbox-init"
|
||||
if readelf -l "$sandbox_init" | grep -q 'Requesting program interpreter' ||
|
||||
! readelf -d "$sandbox_init" 2>&1 | grep -q 'There is no dynamic section'; then
|
||||
printf '%s\n' "install-to-host: sandbox init is dynamically linked" >&2
|
||||
exit 1
|
||||
fi
|
||||
install -m 755 "$sandbox_init" "$install_dir/lazaret-sandbox-init"
|
||||
|
||||
runsc_build_id=9e711e005f13402e728a0c3ada3fdc67a38918da31f720e5be1006edc4272a51
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ fail() {
|
|||
exit 1
|
||||
}
|
||||
|
||||
for command in curl make patch sha256sum tar; do
|
||||
for command in curl patch sha256sum tar; do
|
||||
command -v "$command" >/dev/null 2>&1 || fail "$command is required"
|
||||
done
|
||||
|
||||
|
|
@ -92,14 +92,39 @@ elif [[ "$(tr -d '\n' <"$source/.lazaret-materialization")" != "$stamp" ]]; then
|
|||
fail 'materialized source identity mismatch'
|
||||
fi
|
||||
|
||||
if [[ "$mode" != source ]]; then
|
||||
[[ "${LAZARET_BUILD_CONTAINER:-}" == 1 ]] || fail 'build and test modes require tools/build'
|
||||
command -v bazel >/dev/null 2>&1 || fail 'bazel is required'
|
||||
[[ "$(bazel --version)" == "bazel $bazel_version" ]] || fail 'Bazel version mismatch'
|
||||
fi
|
||||
if [[ "$mode" == test ]]; then
|
||||
make -C "$source" test \
|
||||
TARGETS='//pkg/sentry/fsimpl/gofer:gofer_test //runsc/fsgofer:lisafs_test //test/syscalls:socket_ip_tcp_generic_loopback_test_native //test/syscalls:socket_ip_tcp_generic_loopback_test_runsc_systrap_hostnet'
|
||||
make -C "$source" test OPTIONS='--test_filter=TestProcessStateIsTerminal' \
|
||||
TARGETS=//runsc/container:container_test
|
||||
(
|
||||
cd "$source"
|
||||
bazel test \
|
||||
--color=no \
|
||||
--curses=no \
|
||||
--incompatible_sandbox_hermetic_tmp=false \
|
||||
--keep_going \
|
||||
--strip=never \
|
||||
--test_output=errors \
|
||||
--verbose_failures=true \
|
||||
//pkg/sentry/fsimpl/gofer:gofer_test \
|
||||
//runsc/fsgofer:lisafs_test \
|
||||
//test/syscalls:socket_ip_tcp_generic_loopback_test_native \
|
||||
//test/syscalls:socket_ip_tcp_generic_loopback_test_runsc_systrap_hostnet
|
||||
bazel test \
|
||||
--color=no \
|
||||
--curses=no \
|
||||
--incompatible_sandbox_hermetic_tmp=false \
|
||||
--strip=never \
|
||||
--test_filter=TestProcessStateIsTerminal \
|
||||
--test_output=errors \
|
||||
--verbose_failures=true \
|
||||
//runsc/container:container_test
|
||||
)
|
||||
fi
|
||||
if [[ "$mode" != source ]]; then
|
||||
make -C "$source" build TARGETS=//runsc:runsc
|
||||
(cd "$source" && bazel build --color=no --curses=no //runsc:runsc)
|
||||
built="$source/bazel-bin/runsc/runsc_/runsc"
|
||||
[[ -x "$built" ]] || fail 'canonical build did not produce runsc'
|
||||
version_output="$($built --version 2>&1)"
|
||||
|
|
|
|||
Loading…
Reference in a new issue