Reuse -v/--volume to mount named/host volumes into -r/--run
Volume names now reject '/', which lets a -v spec used with -r be told apart as either an existing named volume or a host directory path. -v becomes repeatable with -r, each mounting a volume at an absolute container path; if the host directory is empty and the image already has content there, it's copied in first (preserving numeric ownership/permissions/links/xattrs-ACLs, degrading gracefully with a warning if the host filesystem doesn't support xattrs). The existence-check and copy run through the same nsenter-wrapped namespace bwrap itself needs, since a rootless containers-storage mount's content isn't otherwise visible to this process at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
@@ -34,7 +34,20 @@ Source layout (all under `src/`):
|
||||
(also `std::filesystem::remove_all()`s the host directory — errors out before
|
||||
touching the config if that fails, warns instead of failing if the directory was
|
||||
already gone) — see `config_file.{h,cpp}` below for what a "volume" means here (a
|
||||
distinct concept from `OciImageConfig::volumes`).
|
||||
distinct concept from `OciImageConfig::volumes`). `-v/--volume` is dual-purpose:
|
||||
used alone it's `create_volume_command()`; combined with `-r/--run` it instead
|
||||
requests a volume mount (repeatable) and is resolved by `resolve_volume_mount()`
|
||||
(see `volume_mount.{h,cpp}` below) instead. Since `-v` must be repeatable with
|
||||
`-r` but each occurrence still takes two space-separated tokens, `main()`'s
|
||||
getopt loop no longer lets `'v'` set `Mode` itself: it accumulates
|
||||
`(spec, path)` pairs into `volume_specs` (consuming the second token manually,
|
||||
with a guard against swallowing the next flag if there isn't one), and only
|
||||
*after* the loop decides whether that means one standalone `Mode::kVolume` call
|
||||
or, together with `-r`, threads `volume_specs` through to `run_container()`.
|
||||
`run_container()` resolves each spec (erroring out, `ok = false`, same as a
|
||||
failed `--user` resolution — `bwrap` is skipped but unmount/cleanup still runs)
|
||||
into a `ResolvedVolumeMount`, rejecting a duplicate or non-absolute container
|
||||
path first, and passes the resolved list to `run_bwrap()`.
|
||||
- `oci_image.{h,cpp}` — validates/parses the OCI Image Layout tar (libarchive +
|
||||
nlohmann_json) and extracts layer blobs. `list_oci_images()` scans a directory
|
||||
(non-recursively) for `*.tar`/`*.tar.*` files and, for each valid OCI archive,
|
||||
@@ -55,6 +68,13 @@ Source layout (all under `src/`):
|
||||
- `bwrap.{h,cpp}` — `detect_bwrap_unshare_args()` probes the kernel (via a forked
|
||||
`unshare(2)` per namespace type) for which `--unshare-xxx` flags `bwrap` can actually
|
||||
use; `build_bwrap_args()`/`run_bwrap()` assemble and run the sandboxed command.
|
||||
`build_bwrap_args()` also takes a `std::vector<ResolvedVolumeMount>` (see
|
||||
`volume_mount.h` below) and appends one writable `--bind <host_directory>
|
||||
<container_path>` per entry. `wrap_for_root_namespace()` is `run_bwrap()`'s own
|
||||
nsenter-wrapping logic pulled out into a reusable, exported function — it's also
|
||||
what `volume_mount.cpp`'s copy-into-an-empty-volume step uses to reach the
|
||||
image's content when running rootless (see below); `run_bwrap()` itself now just
|
||||
calls it once on the assembled `bwrap` argv.
|
||||
`build_bwrap_args()` deliberately drops `--unshare-net` from what's actually passed
|
||||
to `bwrap` even when the kernel supports it — without any network setup (e.g.
|
||||
`slirp4netns`), unsharing it just leaves the sandbox with no network at all. Re-add
|
||||
@@ -128,9 +148,28 @@ Source layout (all under `src/`):
|
||||
untouched. **`VolumeEntry`/the `volumes` section is a distinct concept from
|
||||
`OciImageConfig::volumes`**: this is a user-defined `name -> host directory`
|
||||
mapping created via `-v/--volume`, not an image's own declared mount points (still
|
||||
unconsumed, see `oci_image.{h,cpp}` above) — the two aren't connected yet, though a
|
||||
future `-r/--run` volume-mounting feature would presumably look volumes up here by
|
||||
name.
|
||||
unconsumed, see `oci_image.{h,cpp}` above). `AppConfig`/`config.volumes` is looked
|
||||
up by name in `resolve_volume_mount()` (`volume_mount.{h,cpp}`, see below), which
|
||||
is how `-r/--run`'s own `-v` usage finds a named volume's host directory.
|
||||
- `volume_mount.{h,cpp}` — `is_valid_volume_name()` (no `/`, checked by both
|
||||
`create_volume_command()` and to tell a `-v` spec's name/path apart) and
|
||||
`resolve_volume_mount()`, called once per `-v` occurrence from `run_container()`
|
||||
when running with `-r`. A spec with no `/` is looked up in `config.volumes` by
|
||||
name (error if unknown); one with `/` is treated as a host directory path and
|
||||
`create_directories()`'d if missing. If the resulting host directory is empty and
|
||||
the image already has non-empty content at the given container path, that content
|
||||
is copied in first. Both the existence check and the copy run as a single
|
||||
`sh -c '[ -d ... ] && cp -a ...'` invocation wrapped through
|
||||
`wrap_for_root_namespace()` (`bwrap.h`) — **not** a plain `std::filesystem` check
|
||||
— because a rootless `containers-storage mount`'s content isn't visible to this
|
||||
process at all without `nsenter`, the same constraint `run_bwrap()` itself works
|
||||
around (see the root-vs-rootless paragraph below). `cp -a
|
||||
--preserve=mode,ownership,timestamps,links[,xattr]` does the copy; whether
|
||||
`,xattr` is included is decided by a direct `setxattr()`/`removexattr()` probe on
|
||||
the host directory (no new library dependency — Linux POSIX ACLs are themselves
|
||||
stored as xattrs, so this one probe stands in for both, logging a single
|
||||
`spdlog::warn` if unsupported). A nonzero `cp` exit is only ever a warning, never
|
||||
fatal — often just an ownership-preservation shortfall when not running as root.
|
||||
|
||||
Errors are logged via `spdlog::error`; every external command is also traced at debug
|
||||
level in `run_process()`/`run_process_foreground()` (`src/process.cpp`) — visible via
|
||||
@@ -145,7 +184,10 @@ leaves the result invisible to a plain shell or child process outside that names
|
||||
Confirmed `containers-storage unshare` does **not** rejoin an already-running mount's
|
||||
namespace; only `nsenter` targeting the live `fuse-overlayfs` daemon's PID does.
|
||||
`-r/--run` handles this automatically by locating that PID and running `bwrap` via
|
||||
`nsenter` into its namespaces. **Running as root sidesteps all of this**: no privilege
|
||||
`nsenter` into its namespaces (`wrap_for_root_namespace()`, `src/bwrap.h`) — reused
|
||||
as-is by `volume_mount.cpp`'s copy-into-an-empty-volume step, since that also needs
|
||||
to read image content that's otherwise invisible outside the same namespace.
|
||||
**Running as root sidesteps all of this**: no privilege
|
||||
reexec is needed, so the mount is already directly visible in the current namespace,
|
||||
and `nsenter --user=...` into it then fails ("reassociate to namespace 'ns/user'
|
||||
failed: Invalid argument") since the caller is already in that same user namespace.
|
||||
|
||||
Reference in New Issue
Block a user