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:
@@ -16,12 +16,15 @@ status); this file stays the dense, file-by-file reference. Still early-stage.
|
|||||||
|
|
||||||
Source layout (all under `src/`):
|
Source layout (all under `src/`):
|
||||||
- `main.cpp` — CLI entry point, dependency checks, orchestration (`mount_image()`,
|
- `main.cpp` — CLI entry point, dependency checks, orchestration (`mount_image()`,
|
||||||
`run_container()`, `cleanup_image()`, `unmount_image()`, `list_images_command()`).
|
`run_container()`, `cleanup_image()`, `unmount_image()`, `list_images_command()`,
|
||||||
`run_container()` unconditionally calls `read_oci_image_config()` and reuses the
|
`create_volume_command()`). `run_container()` unconditionally calls
|
||||||
result for two independent defaults: the command to run (`Entrypoint ++ Cmd`) when
|
`read_oci_image_config()` and reuses the result for two independent defaults: the
|
||||||
none is given on the command line, and, when `--user` wasn't given, the sandboxed
|
command to run (`Entrypoint ++ Cmd`) when none is given on the command line, and,
|
||||||
process's user/group (`config.User`, split into `OciImageConfig::user`/`group`) —
|
when `--user` wasn't given, the sandboxed process's user/group (`config.User`,
|
||||||
an explicit `--user`/`--group` on the command line always takes precedence.
|
split into `OciImageConfig::user`/`group`) — an explicit `--user`/`--group` on the
|
||||||
|
command line always takes precedence. `create_volume_command()` implements
|
||||||
|
`-v/--volume <name> <directory>`: see `config_file.{h,cpp}` below for what a
|
||||||
|
"volume" means here (a distinct concept from `OciImageConfig::volumes`).
|
||||||
- `oci_image.{h,cpp}` — validates/parses the OCI Image Layout tar (libarchive +
|
- `oci_image.{h,cpp}` — validates/parses the OCI Image Layout tar (libarchive +
|
||||||
nlohmann_json) and extracts layer blobs. `list_oci_images()` scans a directory
|
nlohmann_json) and extracts layer blobs. `list_oci_images()` scans a directory
|
||||||
(non-recursively) for `*.tar`/`*.tar.*` files and, for each valid OCI archive,
|
(non-recursively) for `*.tar`/`*.tar.*` files and, for each valid OCI archive,
|
||||||
@@ -96,17 +99,27 @@ Source layout (all under `src/`):
|
|||||||
run would skip `run_container()`'s unmount/cleanup entirely, leaving the layer
|
run would skip `run_container()`'s unmount/cleanup entirely, leaving the layer
|
||||||
imported and/or mounted.
|
imported and/or mounted.
|
||||||
- `config_file.{h,cpp}` — `load_config_file()` reads and parses (via libyaml's
|
- `config_file.{h,cpp}` — `load_config_file()` reads and parses (via libyaml's
|
||||||
document API, `<yaml.h>`) the `global` section of the local YAML config file
|
document API, `<yaml.h>`) the `global` and `volumes` sections of the local YAML
|
||||||
located by `config_file_path()` (`$XDG_CONFIG_HOME/slocker-lite/config.yaml`,
|
config file located by `config_file_path()` (`$XDG_CONFIG_HOME/slocker-lite/config.yaml`,
|
||||||
falling back to `$HOME/.config/slocker-lite/config.yaml`). Only `global.log-level`
|
falling back to `$HOME/.config/slocker-lite/config.yaml`). `global.log-level` is the
|
||||||
is supported today — other long options are one-shot flags, not settings, so they
|
only supported `global` key — other long options are one-shot flags, not settings,
|
||||||
don't belong in a persistent config file. A missing file returns a
|
so they don't belong in a persistent config file. A missing file returns a
|
||||||
default-constructed (empty) `AppConfig`, not an error; unknown sections/keys are
|
default-constructed (empty) `AppConfig`, not an error; unknown sections/keys (and
|
||||||
ignored for forward-compatibility; malformed YAML syntax is a hard error. `main()`
|
malformed individual volume entries) are ignored for forward-compatibility;
|
||||||
applies `config->log_level` (via the existing `apply_log_level()`) right after
|
malformed YAML syntax is a hard error. `main()` applies `config->log_level` (via
|
||||||
`spdlog::cfg::load_env_levels()` and before parsing CLI options, so an explicit
|
the existing `apply_log_level()`) right after `spdlog::cfg::load_env_levels()` and
|
||||||
`--log-level` on the command line always overwrites it afterward — same precedence
|
before parsing CLI options, so an explicit `--log-level` on the command line always
|
||||||
pattern already used for `SPDLOG_LEVEL`.
|
overwrites it afterward — same precedence pattern already used for `SPDLOG_LEVEL`.
|
||||||
|
`write_config_file()` writes the whole file back out (via libyaml's
|
||||||
|
document-building/emitter API, symmetric to the read side) — used by
|
||||||
|
`-v/--volume` (`create_volume_command()`, `main.cpp`) to persist a new
|
||||||
|
`VolumeEntry {name, directory}` into the `volumes` section, preserving `global`
|
||||||
|
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.
|
||||||
|
|
||||||
Errors are logged via `spdlog::error`; every external command is also traced at debug
|
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
|
level in `run_process()`/`run_process_foreground()` (`src/process.cpp`) — visible via
|
||||||
|
|||||||
@@ -17,9 +17,11 @@ running kernel actually supports, instead of requiring the full set.
|
|||||||
## Status
|
## Status
|
||||||
|
|
||||||
Early-stage. Mounting, running, and dropping privileges to a specific user/group all
|
Early-stage. Mounting, running, and dropping privileges to a specific user/group all
|
||||||
work. Volumes and image-declared networking (`Volumes`/`ExposedPorts`/`Env` from the
|
work. Named volumes (`-v/--volume`) can be created and are persisted in the config
|
||||||
image config) are parsed but not yet applied, and there's no background/daemonized
|
file, but aren't consumed by `-r/--run` yet. Image-declared networking
|
||||||
run mode yet.
|
(`ExposedPorts`/`Env` from the image config, and the image's own separately-declared
|
||||||
|
`Volumes`) are parsed but not yet applied, and there's no background/daemonized run
|
||||||
|
mode yet.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -55,6 +57,7 @@ slocker-lite -r|--run <image.tar> [-- <command> [args...]]
|
|||||||
slocker-lite -u|--umount <layer-id>
|
slocker-lite -u|--umount <layer-id>
|
||||||
slocker-lite -c|--cleanup <layer-id>
|
slocker-lite -c|--cleanup <layer-id>
|
||||||
slocker-lite -l|--list-images <directory>
|
slocker-lite -l|--list-images <directory>
|
||||||
|
slocker-lite -v|--volume <name> <directory>
|
||||||
slocker-lite -t|--test
|
slocker-lite -t|--test
|
||||||
slocker-lite -h|--help
|
slocker-lite -h|--help
|
||||||
slocker-lite -V|--version
|
slocker-lite -V|--version
|
||||||
@@ -70,6 +73,7 @@ slocker-lite -V|--version
|
|||||||
| `--user <user>` | With `--run`, run the command as this user (name or numeric uid) instead of the image's own declared user (or root, if it declares none). Resolved against the image's own `/etc/passwd`. Only takes effect when `--run` executes as root. |
|
| `--user <user>` | With `--run`, run the command as this user (name or numeric uid) instead of the image's own declared user (or root, if it declares none). Resolved against the image's own `/etc/passwd`. Only takes effect when `--run` executes as root. |
|
||||||
| `--group <group>` | With `--user`, use this group (name or numeric gid) instead of the user's primary group. |
|
| `--group <group>` | With `--user`, use this group (name or numeric gid) instead of the user's primary group. |
|
||||||
| `-l, --list-images <dir>` | List OCI Image Layout tars (`*.tar`, `*.tar.*`) found directly in `<dir>`, with their `name:tag`. |
|
| `-l, --list-images <dir>` | List OCI Image Layout tars (`*.tar`, `*.tar.*`) found directly in `<dir>`, with their `name:tag`. |
|
||||||
|
| `-v, --volume <name> <dir>` | Create a named volume mapped to a host directory (created if missing), recorded in the config file's `volumes` section. Fails if the name or directory is already used by an existing volume. |
|
||||||
| `-t, --test` | Print which `bwrap --unshare-xxx` namespaces the running kernel supports. |
|
| `-t, --test` | Print which `bwrap --unshare-xxx` namespaces the running kernel supports. |
|
||||||
| `--log-level <level>` | Set log verbosity (`trace`, `debug`, `info`, `warn`, `error`, `critical`, `off`). |
|
| `--log-level <level>` | Set log verbosity (`trace`, `debug`, `info`, `warn`, `error`, `critical`, `off`). |
|
||||||
| `-h, --help` | Print usage and exit. |
|
| `-h, --help` | Print usage and exit. |
|
||||||
@@ -92,6 +96,9 @@ sudo ./buildDir/slocker-lite -r myimage.tar --user git
|
|||||||
|
|
||||||
# List every OCI image tar in a directory
|
# List every OCI image tar in a directory
|
||||||
./buildDir/slocker-lite -l ./images
|
./buildDir/slocker-lite -l ./images
|
||||||
|
|
||||||
|
# Create a named volume backed by a host directory
|
||||||
|
./buildDir/slocker-lite -v mydata ~/slocker-volumes/mydata
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
@@ -99,17 +106,21 @@ sudo ./buildDir/slocker-lite -r myimage.tar --user git
|
|||||||
Persistent settings can be kept in a local YAML config file at
|
Persistent settings can be kept in a local YAML config file at
|
||||||
`$XDG_CONFIG_HOME/slocker-lite/config.yaml` (falling back to
|
`$XDG_CONFIG_HOME/slocker-lite/config.yaml` (falling back to
|
||||||
`$HOME/.config/slocker-lite/config.yaml` if `XDG_CONFIG_HOME` isn't set). The file is
|
`$HOME/.config/slocker-lite/config.yaml` if `XDG_CONFIG_HOME` isn't set). The file is
|
||||||
organized into sections; only `global` exists today:
|
organized into sections:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
global:
|
global:
|
||||||
log-level: debug
|
log-level: debug
|
||||||
|
volumes:
|
||||||
|
mydata: /home/user/slocker-volumes/mydata
|
||||||
```
|
```
|
||||||
|
|
||||||
Only options that make sense as a standing preference are supported here — right now
|
`global.log-level` is the only standing preference supported today (one-shot
|
||||||
just `log-level` (one-shot commands like `--mount`/`--run`/`--user` don't belong in a
|
commands like `--mount`/`--run`/`--user` don't belong in a config file). An explicit
|
||||||
config file). A missing config file is fine (nothing is overridden); an explicit
|
`--log-level` on the command line always overrides the config file. The `volumes`
|
||||||
`--log-level` on the command line always overrides the config file.
|
section is managed by `-v/--volume` (see above) rather than hand-edited — it's not
|
||||||
|
consumed by `-r/--run` yet. A missing config file is fine either way (nothing is
|
||||||
|
overridden, and one gets created the first time `-v/--volume` is used).
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,17 @@ const yaml_node_t* find_in_mapping(yaml_document_t& document, const yaml_node_t&
|
|||||||
return nullptr;
|
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
|
} // namespace
|
||||||
|
|
||||||
std::filesystem::path config_file_path() {
|
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);
|
yaml_document_delete(&document);
|
||||||
return config;
|
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 <filesystem>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#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
|
// 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
|
// -m/-r/--user don't belong here). Only the "global" section's log-level and the
|
||||||
// supported today; add more optional fields as more long options gain config-file
|
// "volumes" section are supported today; add more optional fields as more long
|
||||||
// support.
|
// options gain config-file support.
|
||||||
struct AppConfig {
|
struct AppConfig {
|
||||||
std::optional<std::string> log_level; // global.log-level
|
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
|
// $XDG_CONFIG_HOME/slocker-lite/config.yaml, or $HOME/.config/slocker-lite/config.yaml
|
||||||
// if XDG_CONFIG_HOME is unset/empty.
|
// if XDG_CONFIG_HOME is unset/empty.
|
||||||
std::filesystem::path config_file_path();
|
std::filesystem::path config_file_path();
|
||||||
|
|
||||||
// Loads and parses `path`'s "global" section. A missing file is not an error --
|
// Loads and parses `path`'s "global" and "volumes" sections. A missing file is not
|
||||||
// returns a default-constructed AppConfig (nothing set). Unknown sections/keys are
|
// an error -- returns a default-constructed AppConfig (nothing set). Unknown
|
||||||
// ignored, so the format stays forward-compatible. Malformed YAML syntax logs a
|
// sections/keys (and malformed individual volume entries) are ignored, so the
|
||||||
// specific error and returns nullopt.
|
// 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);
|
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"};
|
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
|
// --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
|
// 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 kUserOpt = 257;
|
||||||
constexpr int kGroupOpt = 258;
|
constexpr int kGroupOpt = 258;
|
||||||
|
|
||||||
constexpr std::array<struct option, 13> kLongOptions = {{
|
constexpr std::array<struct option, 14> kLongOptions = {{
|
||||||
{"help", no_argument, nullptr, 'h'},
|
{"help", no_argument, nullptr, 'h'},
|
||||||
{"version", no_argument, nullptr, 'V'},
|
{"version", no_argument, nullptr, 'V'},
|
||||||
{"test", no_argument, nullptr, 't'},
|
{"test", no_argument, nullptr, 't'},
|
||||||
@@ -63,6 +63,7 @@ constexpr std::array<struct option, 13> kLongOptions = {{
|
|||||||
{"list-images", required_argument, nullptr, 'l'},
|
{"list-images", required_argument, nullptr, 'l'},
|
||||||
{"user", required_argument, nullptr, kUserOpt},
|
{"user", required_argument, nullptr, kUserOpt},
|
||||||
{"group", required_argument, nullptr, kGroupOpt},
|
{"group", required_argument, nullptr, kGroupOpt},
|
||||||
|
{"volume", required_argument, nullptr, 'v'},
|
||||||
{nullptr, 0, nullptr, 0},
|
{nullptr, 0, nullptr, 0},
|
||||||
}};
|
}};
|
||||||
|
|
||||||
@@ -73,6 +74,7 @@ void print_usage(const char* prog) {
|
|||||||
" {0} -u|--umount <layer-id>\n"
|
" {0} -u|--umount <layer-id>\n"
|
||||||
" {0} -c|--cleanup <layer-id>\n"
|
" {0} -c|--cleanup <layer-id>\n"
|
||||||
" {0} -l|--list-images <directory>\n"
|
" {0} -l|--list-images <directory>\n"
|
||||||
|
" {0} -v|--volume <name> <directory>\n"
|
||||||
" {0} -t|--test\n"
|
" {0} -t|--test\n"
|
||||||
" {0} -h|--help\n"
|
" {0} -h|--help\n"
|
||||||
" {0} -V|--version\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"
|
" gid) instead of the user's own primary group\n"
|
||||||
" -l, --list-images <dir> list OCI Image Layout tars (*.tar, *.tar.*) found\n"
|
" -l, --list-images <dir> list OCI Image Layout tars (*.tar, *.tar.*) found\n"
|
||||||
" directly in <dir>, with their name:tag\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"
|
" -t, --test run the test suite\n"
|
||||||
" --log-level <level> set log verbosity (trace, debug, info, warn,\n"
|
" --log-level <level> set log verbosity (trace, debug, info, warn,\n"
|
||||||
" error, critical, off)\n"
|
" error, critical, off)\n"
|
||||||
@@ -256,6 +261,45 @@ int list_images_command(const std::filesystem::path& dir) {
|
|||||||
return 0;
|
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,
|
int run_container(const std::filesystem::path& image_tar,
|
||||||
const std::vector<std::string>& requested_command, bool use_nsenter,
|
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) {
|
||||||
@@ -312,7 +356,8 @@ int run_container(const std::filesystem::path& image_tar,
|
|||||||
int main(int argc, char* argv[]) {
|
int main(int argc, char* argv[]) {
|
||||||
spdlog::cfg::load_env_levels();
|
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) {
|
if (!config) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -328,7 +373,7 @@ int main(int argc, char* argv[]) {
|
|||||||
|
|
||||||
opterr = 0;
|
opterr = 0;
|
||||||
int opt;
|
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) {
|
switch (opt) {
|
||||||
case 'h':
|
case 'h':
|
||||||
print_usage(argv[0]);
|
print_usage(argv[0]);
|
||||||
@@ -341,7 +386,8 @@ int main(int argc, char* argv[]) {
|
|||||||
case 'u':
|
case 'u':
|
||||||
case 'r':
|
case 'r':
|
||||||
case 'c':
|
case 'c':
|
||||||
case 'l': {
|
case 'l':
|
||||||
|
case 'v': {
|
||||||
Mode requested;
|
Mode requested;
|
||||||
switch (opt) {
|
switch (opt) {
|
||||||
case 't':
|
case 't':
|
||||||
@@ -359,9 +405,12 @@ int main(int argc, char* argv[]) {
|
|||||||
case 'c':
|
case 'c':
|
||||||
requested = Mode::kCleanup;
|
requested = Mode::kCleanup;
|
||||||
break;
|
break;
|
||||||
default:
|
case 'l':
|
||||||
requested = Mode::kListImages;
|
requested = Mode::kListImages;
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
requested = Mode::kVolume;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (mode != Mode::kNone && mode != requested) {
|
if (mode != Mode::kNone && mode != requested) {
|
||||||
spdlog::error("multiple actions specified");
|
spdlog::error("multiple actions specified");
|
||||||
@@ -404,7 +453,13 @@ int main(int argc, char* argv[]) {
|
|||||||
print_usage(argv[0]);
|
print_usage(argv[0]);
|
||||||
return 1;
|
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]);
|
print_usage(argv[0]);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -426,6 +481,9 @@ int main(int argc, char* argv[]) {
|
|||||||
if (mode == Mode::kListImages) {
|
if (mode == Mode::kListImages) {
|
||||||
return list_images_command(mode_arg);
|
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) {
|
if (mode == Mode::kRun) {
|
||||||
std::vector<std::string> command(argv + optind, argv + argc);
|
std::vector<std::string> command(argv + optind, argv + argc);
|
||||||
// As root, containers-storage mount doesn't need to reexec into a private
|
// As root, containers-storage mount doesn't need to reexec into a private
|
||||||
|
|||||||
Reference in New Issue
Block a user