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:
+33
-19
@@ -178,6 +178,7 @@ std::vector<std::string> detect_bwrap_unshare_args() {
|
||||
|
||||
std::vector<std::string> build_bwrap_args(const std::string& root,
|
||||
const std::vector<std::string>& command,
|
||||
const std::vector<ResolvedVolumeMount>& volumes,
|
||||
std::optional<ResolvedUser> user) {
|
||||
// --new-session detaches from the controlling terminal, which breaks job
|
||||
// control for an interactive foreground shell ("can't access tty"). Re-enable
|
||||
@@ -232,6 +233,10 @@ std::vector<std::string> build_bwrap_args(const std::string& root,
|
||||
};
|
||||
args.insert(args.end(), filesystem_args.begin(), filesystem_args.end());
|
||||
|
||||
for (const auto& volume : volumes) {
|
||||
args.insert(args.end(), {"--bind", volume.host_directory, volume.container_path});
|
||||
}
|
||||
|
||||
if (const char* term = std::getenv("TERM")) {
|
||||
args.insert(args.end(), {"--setenv", "TERM", term});
|
||||
}
|
||||
@@ -262,32 +267,41 @@ std::vector<std::string> build_bwrap_args(const std::string& root,
|
||||
return args;
|
||||
}
|
||||
|
||||
int run_bwrap(const std::string& root, const std::vector<std::string>& command, bool use_nsenter,
|
||||
std::optional<ResolvedUser> user) {
|
||||
std::vector<std::string> argv;
|
||||
std::optional<std::vector<std::string>> wrap_for_root_namespace(const std::string& root, bool use_nsenter,
|
||||
const std::vector<std::string>& argv) {
|
||||
if (!use_nsenter) {
|
||||
return argv;
|
||||
}
|
||||
|
||||
auto pid = find_fuse_overlayfs_pid(root);
|
||||
if (!pid) {
|
||||
spdlog::error("could not find the fuse-overlayfs process serving {}", root);
|
||||
return std::nullopt;
|
||||
}
|
||||
if (!find_in_path("nsenter")) {
|
||||
spdlog::error("nsenter not found in PATH");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<std::string> wrapped = {"nsenter", fmt::format("--user=/proc/{}/ns/user", *pid),
|
||||
fmt::format("--mount=/proc/{}/ns/mnt", *pid), "--"};
|
||||
wrapped.insert(wrapped.end(), argv.begin(), argv.end());
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
int run_bwrap(const std::string& root, const std::vector<std::string>& command, bool use_nsenter,
|
||||
const std::vector<ResolvedVolumeMount>& volumes, std::optional<ResolvedUser> user) {
|
||||
if (user && !find_priv_drop_helper()) {
|
||||
spdlog::error("could not find the {} helper next to this binary; --user/--group requires it",
|
||||
kPrivDropHelperName);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (use_nsenter) {
|
||||
auto pid = find_fuse_overlayfs_pid(root);
|
||||
if (!pid) {
|
||||
spdlog::error("could not find the fuse-overlayfs process serving {}", root);
|
||||
return -1;
|
||||
}
|
||||
if (!find_in_path("nsenter")) {
|
||||
spdlog::error("nsenter not found in PATH");
|
||||
return -1;
|
||||
}
|
||||
argv = {"nsenter", fmt::format("--user=/proc/{}/ns/user", *pid),
|
||||
fmt::format("--mount=/proc/{}/ns/mnt", *pid), "--"};
|
||||
auto bwrap_args = build_bwrap_args(root, command, volumes, user);
|
||||
auto argv = wrap_for_root_namespace(root, use_nsenter, bwrap_args);
|
||||
if (!argv) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto bwrap_args = build_bwrap_args(root, command, user);
|
||||
argv.insert(argv.end(), bwrap_args.begin(), bwrap_args.end());
|
||||
|
||||
return run_process_foreground(argv);
|
||||
return run_process_foreground(*argv);
|
||||
}
|
||||
|
||||
+23
-5
@@ -20,6 +20,8 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "volume_mount.h"
|
||||
|
||||
// Probes the running kernel for which Linux namespace types can actually be
|
||||
// unshared and returns the corresponding bwrap --unshare-xxx flags for the
|
||||
// ones that are supported. Intended for kernels with partial namespace
|
||||
@@ -27,6 +29,19 @@
|
||||
// --unshare-xxx flag to bwrap would make it fail outright.
|
||||
std::vector<std::string> detect_bwrap_unshare_args();
|
||||
|
||||
// Wraps `argv` so it runs inside the mount+user namespace that containers-storage's
|
||||
// rootless fuse-overlayfs daemon created for `root` (the merged mount path from
|
||||
// mount_layer()), via nsenter -- needed to reach anything under `root` at all from a
|
||||
// plain process outside that namespace (containers-storage mount reexecs itself
|
||||
// into a private namespace to get the privilege an unprivileged overlay mount
|
||||
// needs; only nsenter targeting that live daemon's PID can rejoin it). If
|
||||
// use_nsenter is false, returns argv unchanged (the mount is already directly
|
||||
// visible, e.g. when running as root -- see run_container() in main.cpp). Returns
|
||||
// nullopt (and logs) if the fuse-overlayfs process or nsenter itself can't be
|
||||
// found.
|
||||
std::optional<std::vector<std::string>> wrap_for_root_namespace(const std::string& root, bool use_nsenter,
|
||||
const std::vector<std::string>& argv);
|
||||
|
||||
struct ResolvedUser {
|
||||
int uid;
|
||||
int gid;
|
||||
@@ -34,12 +49,15 @@ struct ResolvedUser {
|
||||
|
||||
// Assembles the full bwrap argv (program name included) to run `command` with
|
||||
// `root` bound as the sandbox's filesystem root, using whichever --unshare-xxx
|
||||
// flags the kernel supports (see detect_bwrap_unshare_args()). If `user` is set,
|
||||
// the command is wrapped so it drops to that uid/gid before running -- see
|
||||
// run_bwrap() for how, since bwrap's own --uid/--gid require --unshare-user, which
|
||||
// isn't requested when running as root (see detect_bwrap_unshare_args()).
|
||||
// flags the kernel supports (see detect_bwrap_unshare_args()). Each entry in
|
||||
// `volumes` is bound writably at its container_path (see resolve_volume_mount()
|
||||
// in volume_mount.h). If `user` is set, the command is wrapped so it drops to that
|
||||
// uid/gid before running -- see run_bwrap() for how, since bwrap's own --uid/--gid
|
||||
// require --unshare-user, which isn't requested when running as root (see
|
||||
// detect_bwrap_unshare_args()).
|
||||
std::vector<std::string> build_bwrap_args(const std::string& root,
|
||||
const std::vector<std::string>& command,
|
||||
const std::vector<ResolvedVolumeMount>& volumes,
|
||||
std::optional<ResolvedUser> user);
|
||||
|
||||
// Runs bwrap against `root` (the merged mount path from mount_layer()) in the
|
||||
@@ -54,4 +72,4 @@ std::vector<std::string> build_bwrap_args(const std::string& root,
|
||||
// used to drop privileges to that uid/gid before running `command` -- see
|
||||
// build_bwrap_args(). Returns bwrap's exit code, or -1 on failure to launch.
|
||||
int run_bwrap(const std::string& root, const std::vector<std::string>& command, bool use_nsenter,
|
||||
std::optional<ResolvedUser> user);
|
||||
const std::vector<ResolvedVolumeMount>& volumes, std::optional<ResolvedUser> user);
|
||||
|
||||
+82
-18
@@ -37,6 +37,7 @@
|
||||
#include "oci_image.h"
|
||||
#include "process.h"
|
||||
#include "user_spec.h"
|
||||
#include "volume_mount.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -92,7 +93,7 @@ constexpr std::array<struct option, 18> kLongOptions = {{
|
||||
void print_usage(const char* prog) {
|
||||
fmt::print(
|
||||
"usage: {0} -m|--mount <image.tar>\n"
|
||||
" {0} -r|--run <image.tar> [-- <command> [args...]]\n"
|
||||
" {0} -r|--run <image.tar> [-v <name-or-dir> <container-path>]... [-- <command> [args...]]\n"
|
||||
" {0} -u|--umount <layer-id>\n"
|
||||
" {0} -c|--cleanup <layer-id>\n"
|
||||
" {0} -l|--list-images <directory>\n"
|
||||
@@ -137,7 +138,14 @@ void print_usage(const char* prog) {
|
||||
" mounting or running it\n"
|
||||
" -v, --volume <name> <dir> create a named volume mapped to a host directory\n"
|
||||
" (created if missing), recorded in the config\n"
|
||||
" file's volumes section\n"
|
||||
" file's volumes section. With --run, instead\n"
|
||||
" mount a volume into the sandbox: <name> is a\n"
|
||||
" named volume or, if it contains '/', a host\n"
|
||||
" directory (created if missing); <dir> is the\n"
|
||||
" absolute path inside the container to mount it\n"
|
||||
" at. May be repeated with --run. If the host\n"
|
||||
" directory is empty and the image already has\n"
|
||||
" content there, it's copied in first\n"
|
||||
" --list-volumes list all named volumes (see -v/--volume) with\n"
|
||||
" their host directory\n"
|
||||
" --delete-volume <name>\n"
|
||||
@@ -351,6 +359,11 @@ int inspect_image_command(const std::filesystem::path& image_tar) {
|
||||
|
||||
int create_volume_command(const std::string& name, const std::string& directory,
|
||||
const std::filesystem::path& config_path, AppConfig& config) {
|
||||
if (!is_valid_volume_name(name)) {
|
||||
spdlog::error("volume name '{}' must not contain '/'", name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::filesystem::path resolved = std::filesystem::absolute(directory).lexically_normal();
|
||||
|
||||
for (const auto& volume : config.volumes) {
|
||||
@@ -439,7 +452,9 @@ int delete_volume_command(const std::string& name, const std::filesystem::path&
|
||||
|
||||
int run_container(const std::filesystem::path& image_tar,
|
||||
const std::vector<std::string>& requested_command, bool use_nsenter,
|
||||
const std::optional<std::string>& user, const std::optional<std::string>& group) {
|
||||
const std::optional<std::string>& user, const std::optional<std::string>& group,
|
||||
const std::vector<std::pair<std::string, std::string>>& volume_specs,
|
||||
const AppConfig& app_config) {
|
||||
auto mounted = mount_image(image_tar);
|
||||
if (!mounted) {
|
||||
return 1;
|
||||
@@ -448,6 +463,35 @@ int run_container(const std::filesystem::path& image_tar,
|
||||
|
||||
auto config = read_oci_image_config(image_tar);
|
||||
|
||||
bool ok = true;
|
||||
|
||||
std::vector<ResolvedVolumeMount> volume_mounts;
|
||||
for (const auto& [spec, container_path] : volume_specs) {
|
||||
if (container_path.empty() || container_path.front() != '/') {
|
||||
spdlog::error("volume container path '{}' must be absolute", container_path);
|
||||
ok = false;
|
||||
continue;
|
||||
}
|
||||
bool duplicate = std::any_of(
|
||||
volume_mounts.begin(), volume_mounts.end(),
|
||||
[&](const ResolvedVolumeMount& mount) { return mount.container_path == container_path; });
|
||||
if (duplicate) {
|
||||
spdlog::error("volume container path '{}' is mounted more than once", container_path);
|
||||
ok = false;
|
||||
continue;
|
||||
}
|
||||
// use_nsenter also governs whether resolve_volume_mount() needs nsenter to see
|
||||
// the image's own content when populating an empty volume -- the same
|
||||
// rootless-mount visibility constraint run_bwrap() itself works around.
|
||||
auto resolved =
|
||||
resolve_volume_mount(spec, container_path, app_config, mounted->merged_path, use_nsenter);
|
||||
if (!resolved) {
|
||||
ok = false;
|
||||
continue;
|
||||
}
|
||||
volume_mounts.push_back(std::move(*resolved));
|
||||
}
|
||||
|
||||
// Falls back to the image's own declared user (config.User) when --user wasn't
|
||||
// given on the command line, rather than always defaulting to root.
|
||||
std::optional<std::string> effective_user = user;
|
||||
@@ -457,7 +501,6 @@ int run_container(const std::filesystem::path& image_tar,
|
||||
effective_group = config->group.empty() ? std::nullopt : std::optional<std::string>(config->group);
|
||||
}
|
||||
|
||||
bool ok = true;
|
||||
std::optional<ResolvedUser> resolved_user;
|
||||
if (effective_user) {
|
||||
resolved_user = resolve_user_and_group(*effective_user, effective_group, mounted->merged_path);
|
||||
@@ -474,7 +517,7 @@ int run_container(const std::filesystem::path& image_tar,
|
||||
|
||||
int exit_code = -1;
|
||||
if (ok) {
|
||||
exit_code = run_bwrap(mounted->merged_path, command, use_nsenter, resolved_user);
|
||||
exit_code = run_bwrap(mounted->merged_path, command, use_nsenter, volume_mounts, resolved_user);
|
||||
if (exit_code < 0) {
|
||||
spdlog::error("failed to run bwrap");
|
||||
}
|
||||
@@ -507,6 +550,7 @@ int main(int argc, char* argv[]) {
|
||||
bool disable_nsenter = false;
|
||||
std::optional<std::string> user_flag;
|
||||
std::optional<std::string> group_flag;
|
||||
std::vector<std::pair<std::string, std::string>> volume_specs;
|
||||
|
||||
opterr = 0;
|
||||
int opt;
|
||||
@@ -524,7 +568,6 @@ int main(int argc, char* argv[]) {
|
||||
case 'r':
|
||||
case 'c':
|
||||
case 'l':
|
||||
case 'v':
|
||||
case 'i':
|
||||
case kListVolumesOpt:
|
||||
case kDeleteVolumeOpt:
|
||||
@@ -549,9 +592,6 @@ int main(int argc, char* argv[]) {
|
||||
case 'l':
|
||||
requested = Mode::kListImages;
|
||||
break;
|
||||
case 'v':
|
||||
requested = Mode::kVolume;
|
||||
break;
|
||||
case 'i':
|
||||
requested = Mode::kInspect;
|
||||
break;
|
||||
@@ -576,6 +616,21 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'v': {
|
||||
// -v/--volume takes two tokens: optarg (the name or host path) plus
|
||||
// the immediately following argv entry (the directory, or, with -r,
|
||||
// the container path). Consumed manually (rather than via getopt's
|
||||
// own required_argument) so -v can repeat with -r -- see main()'s
|
||||
// post-loop handling for how the two uses are told apart.
|
||||
if (optind >= argc || (argv[optind][0] == '-' && argv[optind][1] != '\0')) {
|
||||
spdlog::error("--volume requires a name/path and a directory or container path");
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
volume_specs.emplace_back(optarg, argv[optind]);
|
||||
++optind;
|
||||
break;
|
||||
}
|
||||
case 'n':
|
||||
disable_nsenter = true;
|
||||
break;
|
||||
@@ -602,17 +657,25 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!volume_specs.empty() && mode != Mode::kRun) {
|
||||
if (mode != Mode::kNone) {
|
||||
spdlog::error("--volume can only be used standalone or together with --run");
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
if (volume_specs.size() > 1) {
|
||||
spdlog::error("--volume can only be used once outside of --run");
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
mode = Mode::kVolume;
|
||||
}
|
||||
|
||||
if (mode == Mode::kNone) {
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
if (mode == Mode::kVolume) {
|
||||
if (optind + 1 != argc) {
|
||||
spdlog::error("--volume requires a name and a directory");
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
} else if (mode != Mode::kRun && optind != argc) {
|
||||
if (mode != Mode::kRun && optind != argc) {
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
@@ -638,7 +701,8 @@ int main(int argc, char* argv[]) {
|
||||
return inspect_image_command(mode_arg);
|
||||
}
|
||||
if (mode == Mode::kVolume) {
|
||||
return create_volume_command(mode_arg, argv[optind], config_path, *config);
|
||||
return create_volume_command(volume_specs.front().first, volume_specs.front().second, config_path,
|
||||
*config);
|
||||
}
|
||||
if (mode == Mode::kListVolumes) {
|
||||
return list_volumes_command(*config);
|
||||
@@ -656,7 +720,7 @@ int main(int argc, char* argv[]) {
|
||||
if (geteuid() == 0 && !disable_nsenter) {
|
||||
spdlog::debug("running as root; skipping nsenter (the mount is already directly visible)");
|
||||
}
|
||||
return run_container(mode_arg, command, use_nsenter, user_flag, group_flag);
|
||||
return run_container(mode_arg, command, use_nsenter, user_flag, group_flag, volume_specs, *config);
|
||||
}
|
||||
|
||||
auto mounted = mount_image(mode_arg);
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
// Copyright (C) 2026 Viorel Munteanu
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along
|
||||
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
#include "volume_mount.h"
|
||||
|
||||
#include <sys/xattr.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <fmt/core.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include "bwrap.h"
|
||||
#include "process.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// Probes whether `dir`'s filesystem supports (user) extended attributes by
|
||||
// setting and immediately removing a throwaway one. Linux POSIX ACLs are
|
||||
// themselves stored as xattrs, so this one probe covers both.
|
||||
bool xattr_supported(const std::filesystem::path& dir) {
|
||||
constexpr const char* kProbeName = "user.slocker-lite.probe";
|
||||
if (setxattr(dir.c_str(), kProbeName, "1", 1, 0) != 0) {
|
||||
return false;
|
||||
}
|
||||
removexattr(dir.c_str(), kProbeName);
|
||||
return true;
|
||||
}
|
||||
|
||||
// If `image_side` (a path under the mounted image root) turns out to be a
|
||||
// non-empty directory, recursively copies its contents into the already-existing
|
||||
// `dst`, preserving mode/ownership/timestamps/links (and xattrs/ACLs where dst's
|
||||
// filesystem supports them). Both the existence check and the copy run as a single
|
||||
// `sh -c` invocation wrapped through wrap_for_root_namespace(): a rootless
|
||||
// containers-storage mount lives in a private namespace this process can't see
|
||||
// into directly (the same reason run_bwrap() itself needs nsenter), so a plain
|
||||
// std::filesystem check on `image_side` from here would see nothing at all. Never
|
||||
// treated as fatal: a nonzero exit is very often just an ownership-preservation
|
||||
// shortfall when not running as root (chown() -> EPERM) rather than a real
|
||||
// failure, and cp's own stderr is inherited straight to the user (see process.h),
|
||||
// so -r/--run proceeds with whatever did copy.
|
||||
void copy_if_present(const std::string& root, bool use_nsenter, const std::filesystem::path& image_side,
|
||||
const std::filesystem::path& dst) {
|
||||
if (!find_in_path("cp") || !find_in_path("sh")) {
|
||||
spdlog::warn("cp/sh not found in PATH; cannot populate volume directory {} from the image",
|
||||
dst.string());
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<std::string> preserve = {"mode", "ownership", "timestamps", "links"};
|
||||
if (xattr_supported(dst)) {
|
||||
preserve.push_back("xattr");
|
||||
} else {
|
||||
spdlog::warn("extended attributes/ACLs are not supported on {}; skipping their preservation",
|
||||
dst.string());
|
||||
}
|
||||
|
||||
std::string preserve_arg = "--preserve=";
|
||||
for (size_t i = 0; i < preserve.size(); ++i) {
|
||||
if (i > 0) {
|
||||
preserve_arg += ',';
|
||||
}
|
||||
preserve_arg += preserve[i];
|
||||
}
|
||||
|
||||
// $0=sh $1=image_side $2=preserve_arg $3=dst. Trailing "/." (not just $1) copies
|
||||
// image_side's *contents* into the already-created dst instead of nesting a
|
||||
// subdirectory inside it.
|
||||
std::vector<std::string> script = {
|
||||
"sh", "-c",
|
||||
"if [ -d \"$1\" ] && [ -n \"$(ls -A \"$1\" 2>/dev/null)\" ]; then cp -a \"$2\" \"$1/.\" \"$3\"; fi",
|
||||
"sh", image_side.string(), preserve_arg, dst.string()};
|
||||
|
||||
auto argv = wrap_for_root_namespace(root, use_nsenter, script);
|
||||
if (!argv) {
|
||||
spdlog::warn("could not reach the image's mount namespace; volume directory {} was not populated",
|
||||
dst.string());
|
||||
return;
|
||||
}
|
||||
|
||||
auto result = run_process(*argv);
|
||||
if (result.exit_code != 0) {
|
||||
spdlog::warn("copying initial contents into {} reported errors (exit code {})", dst.string(),
|
||||
result.exit_code);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool is_valid_volume_name(std::string_view name) {
|
||||
return !name.empty() && name.find('/') == std::string_view::npos;
|
||||
}
|
||||
|
||||
std::optional<ResolvedVolumeMount> resolve_volume_mount(const std::string& spec,
|
||||
const std::string& container_path,
|
||||
const AppConfig& config,
|
||||
const std::string& merged_path, bool use_nsenter) {
|
||||
std::string host_directory;
|
||||
|
||||
if (is_valid_volume_name(spec)) {
|
||||
auto it = std::find_if(config.volumes.begin(), config.volumes.end(),
|
||||
[&](const VolumeEntry& volume) { return volume.name == spec; });
|
||||
if (it == config.volumes.end()) {
|
||||
spdlog::error("no volume named '{}' exists", spec);
|
||||
return std::nullopt;
|
||||
}
|
||||
host_directory = it->directory;
|
||||
} else {
|
||||
std::filesystem::path resolved = std::filesystem::absolute(spec).lexically_normal();
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(resolved, ec);
|
||||
if (ec) {
|
||||
spdlog::error("failed to create directory {}: {}", resolved.string(), ec.message());
|
||||
return std::nullopt;
|
||||
}
|
||||
host_directory = resolved.string();
|
||||
}
|
||||
|
||||
std::error_code empty_ec;
|
||||
if (std::filesystem::is_empty(host_directory, empty_ec) && !empty_ec) {
|
||||
// std::filesystem::path::operator/ discards the lhs entirely when the rhs is
|
||||
// absolute, so relative_path() strips container_path's leading '/' first.
|
||||
std::filesystem::path image_side =
|
||||
std::filesystem::path(merged_path) / std::filesystem::path(container_path).relative_path();
|
||||
copy_if_present(merged_path, use_nsenter, image_side, host_directory);
|
||||
}
|
||||
|
||||
return ResolvedVolumeMount{host_directory, container_path};
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright (C) 2026 Viorel Munteanu
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along
|
||||
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "config_file.h"
|
||||
|
||||
// True if `name` is non-empty and contains no '/'. Volume names must satisfy this
|
||||
// so that -v/--volume's first parameter (used with -r/--run) can be told apart from
|
||||
// a host directory path: a name never contains '/', a host path always does.
|
||||
bool is_valid_volume_name(std::string_view name);
|
||||
|
||||
// A -v spec, resolved and ready to hand to build_bwrap_args()/run_bwrap().
|
||||
struct ResolvedVolumeMount {
|
||||
std::string host_directory; // absolute, already exists
|
||||
std::string container_path; // as given on the command line
|
||||
};
|
||||
|
||||
// Resolves one -v <spec> <container_path> used together with -r/--run. `spec` is
|
||||
// either an existing named volume (no '/', looked up in config.volumes) or a host
|
||||
// directory path (contains '/', created via create_directories() if missing). If
|
||||
// the resulting host directory is empty and the image already has non-empty
|
||||
// content at `container_path` (under `merged_path`, the mounted image root), that
|
||||
// content is copied in first, preserving numeric ownership/permissions/links and,
|
||||
// where the host filesystem supports it, extended attributes/ACLs (a single
|
||||
// warning is logged and they're skipped otherwise). `use_nsenter` must match the
|
||||
// value run_bwrap() will use for this same run: reading anything under
|
||||
// `merged_path` requires it whenever the mount was made rootless (see
|
||||
// wrap_for_root_namespace() in bwrap.h), exactly like the eventual bwrap run
|
||||
// itself. Logs a specific error and returns nullopt on a hard failure (unknown
|
||||
// volume name, can't create the host directory); a failed or degraded copy is only
|
||||
// ever a warning, never fatal here.
|
||||
std::optional<ResolvedVolumeMount> resolve_volume_mount(const std::string& spec,
|
||||
const std::string& container_path,
|
||||
const AppConfig& config,
|
||||
const std::string& merged_path, bool use_nsenter);
|
||||
Reference in New Issue
Block a user