Add -v/--volume to create a named volume mapped to a host directory
Creates the host directory if missing (warning if it already exists,
another if it's non-empty) and records name -> directory in the config
file's new "volumes" section. Fails if the name or directory is already
used by an existing volume.
This is a distinct concept from OciImageConfig::volumes (an image's own
declared mount points, still unconsumed) -- a user-defined volume, meant
to be referenced by name once -r/--run starts actually mounting volumes.
config_file.{h,cpp} gains write_config_file(), symmetric to the existing
load_config_file(), built on libyaml's document-building/emitter API.
Rewrites the whole file each time; global.log-level round-trips
untouched alongside the new volumes section.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
@@ -44,6 +44,17 @@ const yaml_node_t* find_in_mapping(yaml_document_t& document, const yaml_node_t&
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int add_scalar(yaml_document_t& document, std::string_view value) {
|
||||
return yaml_document_add_scalar(&document, reinterpret_cast<yaml_char_t*>(const_cast<char*>(YAML_STR_TAG)),
|
||||
reinterpret_cast<const yaml_char_t*>(value.data()),
|
||||
static_cast<int>(value.size()), YAML_PLAIN_SCALAR_STYLE);
|
||||
}
|
||||
|
||||
int add_mapping(yaml_document_t& document) {
|
||||
return yaml_document_add_mapping(&document, reinterpret_cast<yaml_char_t*>(const_cast<char*>(YAML_MAP_TAG)),
|
||||
YAML_BLOCK_MAPPING_STYLE);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::filesystem::path config_file_path() {
|
||||
@@ -88,8 +99,82 @@ std::optional<AppConfig> load_config_file(const std::filesystem::path& path) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (const yaml_node_t* volumes = find_in_mapping(document, *root, "volumes")) {
|
||||
if (volumes->type == YAML_MAPPING_NODE) {
|
||||
for (auto* pair = volumes->data.mapping.pairs.start;
|
||||
pair < volumes->data.mapping.pairs.top; ++pair) {
|
||||
yaml_node_t* key_node = yaml_document_get_node(&document, pair->key);
|
||||
yaml_node_t* value_node = yaml_document_get_node(&document, pair->value);
|
||||
if (key_node && key_node->type == YAML_SCALAR_NODE && value_node &&
|
||||
value_node->type == YAML_SCALAR_NODE) {
|
||||
config.volumes.push_back(
|
||||
{std::string(scalar_value(*key_node)), std::string(scalar_value(*value_node))});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
yaml_document_delete(&document);
|
||||
return config;
|
||||
}
|
||||
|
||||
bool write_config_file(const std::filesystem::path& path, const AppConfig& config) {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(path.parent_path(), ec);
|
||||
if (ec) {
|
||||
spdlog::error("failed to create directory {}: {}", path.parent_path().string(), ec.message());
|
||||
return false;
|
||||
}
|
||||
|
||||
yaml_document_t document;
|
||||
yaml_document_initialize(&document, nullptr, nullptr, nullptr, 1, 1);
|
||||
|
||||
int root = add_mapping(document);
|
||||
|
||||
if (config.log_level) {
|
||||
int global = add_mapping(document);
|
||||
yaml_document_append_mapping_pair(&document, global, add_scalar(document, "log-level"),
|
||||
add_scalar(document, *config.log_level));
|
||||
yaml_document_append_mapping_pair(&document, root, add_scalar(document, "global"), global);
|
||||
}
|
||||
|
||||
if (!config.volumes.empty()) {
|
||||
int volumes = add_mapping(document);
|
||||
for (const auto& volume : config.volumes) {
|
||||
yaml_document_append_mapping_pair(&document, volumes, add_scalar(document, volume.name),
|
||||
add_scalar(document, volume.directory));
|
||||
}
|
||||
yaml_document_append_mapping_pair(&document, root, add_scalar(document, "volumes"), volumes);
|
||||
}
|
||||
|
||||
FILE* file = std::fopen(path.c_str(), "w");
|
||||
if (!file) {
|
||||
spdlog::error("failed to open {} for writing", path.string());
|
||||
yaml_document_delete(&document);
|
||||
return false;
|
||||
}
|
||||
|
||||
yaml_emitter_t emitter;
|
||||
yaml_emitter_initialize(&emitter);
|
||||
yaml_emitter_set_output_file(&emitter, file);
|
||||
|
||||
bool ok = yaml_emitter_open(&emitter) != 0;
|
||||
if (!ok) {
|
||||
// yaml_emitter_dump() below is what normally consumes/destroys `document` --
|
||||
// since open failed and dump never runs, it must be deleted explicitly here.
|
||||
yaml_document_delete(&document);
|
||||
} else {
|
||||
ok = yaml_emitter_dump(&emitter, &document) != 0;
|
||||
ok = yaml_emitter_close(&emitter) != 0 && ok;
|
||||
}
|
||||
|
||||
yaml_emitter_delete(&emitter);
|
||||
std::fclose(file);
|
||||
|
||||
if (!ok) {
|
||||
spdlog::error("failed to write config file {}", path.string());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
+24
-7
@@ -19,21 +19,38 @@
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// A user-defined named volume (-v/--volume): maps `name` to a host `directory`.
|
||||
// Unrelated to OciImageConfig::volumes, which are mount points an *image* declares
|
||||
// it wants -- this is a separate, user-driven concept, tracked here so it can be
|
||||
// referenced by name later once -r/--run starts consuming volumes.
|
||||
struct VolumeEntry {
|
||||
std::string name;
|
||||
std::string directory; // stored absolute + lexically-normalized
|
||||
};
|
||||
|
||||
// Fields that make sense to persist across invocations (one-shot flags like
|
||||
// -m/-r/--user don't belong here). Only the "global" section's log-level is
|
||||
// supported today; add more optional fields as more long options gain config-file
|
||||
// support.
|
||||
// -m/-r/--user don't belong here). Only the "global" section's log-level and the
|
||||
// "volumes" section are supported today; add more optional fields as more long
|
||||
// options gain config-file support.
|
||||
struct AppConfig {
|
||||
std::optional<std::string> log_level; // global.log-level
|
||||
std::vector<VolumeEntry> volumes; // volumes section: name -> directory
|
||||
};
|
||||
|
||||
// $XDG_CONFIG_HOME/slocker-lite/config.yaml, or $HOME/.config/slocker-lite/config.yaml
|
||||
// if XDG_CONFIG_HOME is unset/empty.
|
||||
std::filesystem::path config_file_path();
|
||||
|
||||
// Loads and parses `path`'s "global" section. A missing file is not an error --
|
||||
// returns a default-constructed AppConfig (nothing set). Unknown sections/keys are
|
||||
// ignored, so the format stays forward-compatible. Malformed YAML syntax logs a
|
||||
// specific error and returns nullopt.
|
||||
// Loads and parses `path`'s "global" and "volumes" sections. A missing file is not
|
||||
// an error -- returns a default-constructed AppConfig (nothing set). Unknown
|
||||
// sections/keys (and malformed individual volume entries) are ignored, so the
|
||||
// format stays forward-compatible. Malformed YAML syntax logs a specific error and
|
||||
// returns nullopt.
|
||||
std::optional<AppConfig> load_config_file(const std::filesystem::path& path);
|
||||
|
||||
// Writes `config` back to `path` as YAML (global + volumes sections), creating
|
||||
// `path`'s parent directory if needed. Rewrites the whole file. Logs a specific
|
||||
// error and returns false on failure.
|
||||
bool write_config_file(const std::filesystem::path& path, const AppConfig& config);
|
||||
|
||||
+65
-7
@@ -41,7 +41,7 @@ namespace {
|
||||
|
||||
constexpr std::array<std::string_view, 2> kRequiredTools = {"containers-storage", "bwrap"};
|
||||
|
||||
enum class Mode { kNone, kMount, kUnmount, kTest, kRun, kCleanup, kListImages };
|
||||
enum class Mode { kNone, kMount, kUnmount, kTest, kRun, kCleanup, kListImages, kVolume };
|
||||
|
||||
// --log-level/--user/--group have no short form (--log-level's was freed up so -l
|
||||
// could become --list-images; -u is already --umount), so they need long-option
|
||||
@@ -50,7 +50,7 @@ constexpr int kLogLevelOpt = 256;
|
||||
constexpr int kUserOpt = 257;
|
||||
constexpr int kGroupOpt = 258;
|
||||
|
||||
constexpr std::array<struct option, 13> kLongOptions = {{
|
||||
constexpr std::array<struct option, 14> kLongOptions = {{
|
||||
{"help", no_argument, nullptr, 'h'},
|
||||
{"version", no_argument, nullptr, 'V'},
|
||||
{"test", no_argument, nullptr, 't'},
|
||||
@@ -63,6 +63,7 @@ constexpr std::array<struct option, 13> kLongOptions = {{
|
||||
{"list-images", required_argument, nullptr, 'l'},
|
||||
{"user", required_argument, nullptr, kUserOpt},
|
||||
{"group", required_argument, nullptr, kGroupOpt},
|
||||
{"volume", required_argument, nullptr, 'v'},
|
||||
{nullptr, 0, nullptr, 0},
|
||||
}};
|
||||
|
||||
@@ -73,6 +74,7 @@ void print_usage(const char* prog) {
|
||||
" {0} -u|--umount <layer-id>\n"
|
||||
" {0} -c|--cleanup <layer-id>\n"
|
||||
" {0} -l|--list-images <directory>\n"
|
||||
" {0} -v|--volume <name> <directory>\n"
|
||||
" {0} -t|--test\n"
|
||||
" {0} -h|--help\n"
|
||||
" {0} -V|--version\n"
|
||||
@@ -104,6 +106,9 @@ void print_usage(const char* prog) {
|
||||
" gid) instead of the user's own primary group\n"
|
||||
" -l, --list-images <dir> list OCI Image Layout tars (*.tar, *.tar.*) found\n"
|
||||
" directly in <dir>, with their name:tag\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"
|
||||
" -t, --test run the test suite\n"
|
||||
" --log-level <level> set log verbosity (trace, debug, info, warn,\n"
|
||||
" error, critical, off)\n"
|
||||
@@ -256,6 +261,45 @@ int list_images_command(const std::filesystem::path& dir) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int create_volume_command(const std::string& name, const std::string& directory,
|
||||
const std::filesystem::path& config_path, AppConfig& config) {
|
||||
std::filesystem::path resolved = std::filesystem::absolute(directory).lexically_normal();
|
||||
|
||||
for (const auto& volume : config.volumes) {
|
||||
if (volume.name == name) {
|
||||
spdlog::error("a volume named '{}' already exists (directory: {})", name, volume.directory);
|
||||
return 1;
|
||||
}
|
||||
if (volume.directory == resolved.string()) {
|
||||
spdlog::error("directory {} is already used by volume '{}'", resolved.string(), volume.name);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
bool created = std::filesystem::create_directories(resolved, ec);
|
||||
if (ec) {
|
||||
spdlog::error("failed to create directory {}: {}", resolved.string(), ec.message());
|
||||
return 1;
|
||||
}
|
||||
if (!created) {
|
||||
spdlog::warn("directory {} already exists", resolved.string());
|
||||
std::error_code empty_ec;
|
||||
bool empty = std::filesystem::is_empty(resolved, empty_ec);
|
||||
if (!empty_ec && !empty) {
|
||||
spdlog::warn("directory {} is not empty", resolved.string());
|
||||
}
|
||||
}
|
||||
|
||||
config.volumes.push_back({name, resolved.string()});
|
||||
if (!write_config_file(config_path, config)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
fmt::print("created volume '{}' -> {}\n", name, resolved.string());
|
||||
return 0;
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -312,7 +356,8 @@ int run_container(const std::filesystem::path& image_tar,
|
||||
int main(int argc, char* argv[]) {
|
||||
spdlog::cfg::load_env_levels();
|
||||
|
||||
auto config = load_config_file(config_file_path());
|
||||
std::filesystem::path config_path = config_file_path();
|
||||
auto config = load_config_file(config_path);
|
||||
if (!config) {
|
||||
return 1;
|
||||
}
|
||||
@@ -328,7 +373,7 @@ int main(int argc, char* argv[]) {
|
||||
|
||||
opterr = 0;
|
||||
int opt;
|
||||
while ((opt = getopt_long(argc, argv, ":hVtm:u:r:c:nl:", kLongOptions.data(), nullptr)) != -1) {
|
||||
while ((opt = getopt_long(argc, argv, ":hVtm:u:r:c:nl:v:", kLongOptions.data(), nullptr)) != -1) {
|
||||
switch (opt) {
|
||||
case 'h':
|
||||
print_usage(argv[0]);
|
||||
@@ -341,7 +386,8 @@ int main(int argc, char* argv[]) {
|
||||
case 'u':
|
||||
case 'r':
|
||||
case 'c':
|
||||
case 'l': {
|
||||
case 'l':
|
||||
case 'v': {
|
||||
Mode requested;
|
||||
switch (opt) {
|
||||
case 't':
|
||||
@@ -359,9 +405,12 @@ int main(int argc, char* argv[]) {
|
||||
case 'c':
|
||||
requested = Mode::kCleanup;
|
||||
break;
|
||||
default:
|
||||
case 'l':
|
||||
requested = Mode::kListImages;
|
||||
break;
|
||||
default:
|
||||
requested = Mode::kVolume;
|
||||
break;
|
||||
}
|
||||
if (mode != Mode::kNone && mode != requested) {
|
||||
spdlog::error("multiple actions specified");
|
||||
@@ -404,7 +453,13 @@ int main(int argc, char* argv[]) {
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
if (mode != Mode::kRun && optind != argc) {
|
||||
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) {
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
@@ -426,6 +481,9 @@ int main(int argc, char* argv[]) {
|
||||
if (mode == Mode::kListImages) {
|
||||
return list_images_command(mode_arg);
|
||||
}
|
||||
if (mode == Mode::kVolume) {
|
||||
return create_volume_command(mode_arg, argv[optind], config_path, *config);
|
||||
}
|
||||
if (mode == Mode::kRun) {
|
||||
std::vector<std::string> command(argv + optind, argv + argc);
|
||||
// As root, containers-storage mount doesn't need to reexec into a private
|
||||
|
||||
Reference in New Issue
Block a user