Add -w/--write-config to bootstrap a fully-populated config file

Writes every supported config option explicitly -- the six
unshare-* bools defaulted via value_or(true), log-level captured
from the actually active spdlog level (so an explicit --log-level
passed alongside -w is persisted too) -- creating the file and its
parent directory if missing, and prints its full path. Existing
values (including volumes) are preserved untouched.

Also fixes a stale README row left over from unplugging the
namespace probe out of -t/--test.
This commit is contained in:
2026-08-30 09:44:03 +00:00
parent bcae8bbcd4
commit ace71e0474
5 changed files with 81 additions and 6 deletions
+18 -2
View File
@@ -115,6 +115,22 @@ Source layout (all under `src/`):
already gone) — see `config_file.{h,cpp}` below for what a "volume" means here (a already gone) — see `config_file.{h,cpp}` below for what a "volume" means here (a
distinct concept from `OciImageConfig::volumes`). `dispatch_command()`'s distinct concept from `OciImageConfig::volumes`). `dispatch_command()`'s
`Mode::volume`/`Mode::delete_volume`/`Mode::delete_volume_full` cases call these. `Mode::volume`/`Mode::delete_volume`/`Mode::delete_volume_full` cases call these.
`write_config_command()` implements `-w/--write-config`: unlike
`create_volume_command()`/`delete_volume_command()`'s use of
`write_config_file()` (which only ever persists `AppConfig` fields that are
already set), this fills in *every* field before writing — the six
`unshare-*` bools via `.value_or(true)`, and `log_level` from the actually
active `spdlog::get_level()` (not merely a default for when unset — this
also captures an explicit `--log-level` passed alongside `-w` on the same
command line, overriding whatever an existing config file's own
`log-level` already was, since `main()`/`parse_args()` already applied it
in that precedence order by the time this runs) — so a bare `-w` bootstraps
a complete, fully-populated config file for hand-editing, and `-w` combined
with other flags captures their effective values into it. `volumes` is left
exactly as loaded — an open-ended list with no "default" entry to
materialize. Prints the config file's full path (`write_config_file()`
already creates the parent directory and the file itself if missing, so no
separate existence check is needed here).
`run_container()` (the `Mode::run` dispatch case) resolves each `-v` spec `run_container()` (the `Mode::run` dispatch case) resolves each `-v` spec
(erroring out, `ok = false`, same as a failed `--user` resolution — `bwrap` (erroring out, `ok = false`, same as a failed `--user` resolution — `bwrap`
is skipped but unmount/cleanup still runs) into a `ResolvedVolumeMount`, is skipped but unmount/cleanup still runs) into a `ResolvedVolumeMount`,
@@ -814,8 +830,8 @@ Build directory is `buildDir/` (already configured).
full flag list: `-m/--mount`, `-r/--run`, `-u/--umount`, `-c/--cleanup`, full flag list: `-m/--mount`, `-r/--run`, `-u/--umount`, `-c/--cleanup`,
`-l/--list-images`, `-i/--inspect`, `-x/--exec`, `--kill`, `-n/--no-nsenter`, `-D/--daemonize`, `-l/--list-images`, `-i/--inspect`, `-x/--exec`, `--kill`, `-n/--no-nsenter`, `-D/--daemonize`,
`--user`, `--group`, `--hostname`, `--env`, `--env-file`, `-v/--volume`, `--list-volumes`, `--delete-volume`, `--user`, `--group`, `--hostname`, `--env`, `--env-file`, `-v/--volume`, `--list-volumes`, `--delete-volume`,
`--delete-volume-full`, `--list-processes`, `--clean-processes`, `-t/--test`, `--log-level`, `--delete-volume-full`, `--list-processes`, `--clean-processes`, `-w/--write-config`,
`-h/--help`, `-V/--version`) `-t/--test`, `--log-level`, `-h/--help`, `-V/--version`)
- Run tests: `meson test -C buildDir` - Run tests: `meson test -C buildDir`
## Code style ## Code style
+10 -1
View File
@@ -94,7 +94,8 @@ slocker-lite -V|--version
| `--delete-volume-full <name>` | Like `--delete-volume`, but also recursively deletes the volume's host directory. | | `--delete-volume-full <name>` | Like `--delete-volume`, but also recursively deletes the volume's host directory. |
| `--list-processes` | List running `--run` sessions found by their pid files under `$XDG_STATE_HOME/slocker-lite/run/`, with their pid, container name, and status (`running` or `exited`). | | `--list-processes` | List running `--run` sessions found by their pid files under `$XDG_STATE_HOME/slocker-lite/run/`, with their pid, container name, and status (`running` or `exited`). |
| `--clean-processes` | Remove stale pid files (see `--list-processes`) left behind by sessions that are no longer running. | | `--clean-processes` | Remove stale pid files (see `--list-processes`) left behind by sessions that are no longer running. |
| `-t, --test` | Print which `bwrap --unshare-xxx` namespaces the running kernel supports. | | `-w, --write-config` | Write a complete config file (creating it, and its parent directory, if missing), filling in every option's current or default value. Useful to bootstrap one for hand-editing. Prints the config file's full path. |
| `-t, --test` | Run the (currently empty) self-test placeholder. |
| `--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. |
| `-V, --version` | Print version information and exit. | | `-V, --version` | Print version information and exit. |
@@ -194,6 +195,14 @@ managed by `-v/--volume` (see above) rather than hand-edited — it's what
is fine either way (nothing is overridden, and one gets created the first is fine either way (nothing is overridden, and one gets created the first
time `-v/--volume` is used). time `-v/--volume` is used).
Run `-w/--write-config` to bootstrap a config file: it writes out every
supported option explicitly (filling in the current or default value for
anything not already set — so the block above is exactly what a fresh `-w`
produces), creating the file and its parent directory if they don't exist
yet, and prints the file's full path. Combine it with other flags to seed
specific values, e.g. `slocker-lite --log-level debug -w` writes
`log-level: debug`.
## How it works ## How it works
Image layers are imported into `containers-storage` (parent-chained) and the Image layers are imported into `containers-storage` (parent-chained) and the
+13 -2
View File
@@ -50,7 +50,7 @@ constexpr int env_file = 266;
constexpr int kill = 267; constexpr int kill = 267;
} // namespace options } // namespace options
constexpr std::array<struct option, 26> long_options = {{ constexpr std::array<struct option, 27> long_options = {{
{"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'},
@@ -76,6 +76,7 @@ constexpr std::array<struct option, 26> long_options = {{
{"clean-processes", no_argument, nullptr, options::clean_processes}, {"clean-processes", no_argument, nullptr, options::clean_processes},
{"env", required_argument, nullptr, options::env}, {"env", required_argument, nullptr, options::env},
{"env-file", required_argument, nullptr, options::env_file}, {"env-file", required_argument, nullptr, options::env_file},
{"write-config", no_argument, nullptr, 'w'},
{nullptr, 0, nullptr, 0}, {nullptr, 0, nullptr, 0},
}}; }};
@@ -95,6 +96,7 @@ void print_usage(const char* prog) {
" {0} --delete-volume-full <name>\n" " {0} --delete-volume-full <name>\n"
" {0} --list-processes\n" " {0} --list-processes\n"
" {0} --clean-processes\n" " {0} --clean-processes\n"
" {0} -w|--write-config\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"
@@ -194,6 +196,11 @@ void print_usage(const char* prog) {
" --clean-processes remove stale pid files (see --list-processes)\n" " --clean-processes remove stale pid files (see --list-processes)\n"
" left behind by sessions that are no longer\n" " left behind by sessions that are no longer\n"
" running\n" " running\n"
" -w, --write-config write a complete config file (creating it, and\n"
" its parent directory, if missing), filling in\n"
" every option's current or default value --\n"
" useful to bootstrap one for hand-editing.\n"
" Prints the config file's full path\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"
@@ -240,7 +247,7 @@ bool apply_log_level(std::string_view name) {
std::optional<int> parse_args(int argc, char* argv[], ParsedArgs& out) { std::optional<int> parse_args(int argc, char* argv[], ParsedArgs& out) {
opterr = 0; opterr = 0;
int opt; int opt;
while ((opt = getopt_long(argc, argv, ":hVtm:u:r:c:nl:v:i:x:D", long_options.data(), nullptr)) != -1) { while ((opt = getopt_long(argc, argv, ":hVtm:u:r:c:nl:v:i:x:Dw", long_options.data(), nullptr)) != -1) {
switch (opt) { switch (opt) {
case 'h': case 'h':
print_usage(argv[0]); print_usage(argv[0]);
@@ -256,6 +263,7 @@ std::optional<int> parse_args(int argc, char* argv[], ParsedArgs& out) {
case 'l': case 'l':
case 'i': case 'i':
case 'x': case 'x':
case 'w':
case options::list_volumes: case options::list_volumes:
case options::delete_volume: case options::delete_volume:
case options::delete_volume_full: case options::delete_volume_full:
@@ -267,6 +275,9 @@ std::optional<int> parse_args(int argc, char* argv[], ParsedArgs& out) {
case 't': case 't':
requested = Mode::test; requested = Mode::test;
break; break;
case 'w':
requested = Mode::write_config;
break;
case 'm': case 'm':
requested = Mode::mount; requested = Mode::mount;
break; break;
+2 -1
View File
@@ -42,7 +42,8 @@ enum class Mode {
list_processes, list_processes,
clean_processes, clean_processes,
exec, exec,
kill kill,
write_config
}; };
// Everything parse_args() extracts from argv, ready to hand to // Everything parse_args() extracts from argv, ready to hand to
+38
View File
@@ -366,6 +366,42 @@ int delete_volume_command(const std::string& name, const std::filesystem::path&
return 0; return 0;
} }
// Writes out every supported config option explicitly, defaulting anything
// currently unset to its effective value, creating the file (and its parent
// directory) if it doesn't exist yet -- unlike create_volume_command()/
// delete_volume_command()'s use of write_config_file(), which only ever
// persists fields the user actually set. Meant to bootstrap a complete config
// file for hand-editing.
int write_config_command(const std::filesystem::path& config_path, const AppConfig& config) {
AppConfig full = config;
// Always the actually active spdlog level -- not merely a default for when
// full.log_level is unset -- so that an explicit --log-level passed
// alongside -w on this same command line is captured too, not shadowed by
// whatever an existing config file's own log-level already was (SPDLOG_LEVEL
// env var, an existing config's log-level, and --log-level are applied in
// that order, earlier, by main()/parse_args(); spdlog::get_level() reflects
// whichever of them won). spdlog::level::to_string_view() returns its own
// string_view_t (fmt::basic_string_view), not std::string_view --
// constructed explicitly from its data()/size() rather than relying on an
// implicit conversion that this fmt/spdlog version doesn't offer.
auto level_name = spdlog::level::to_string_view(spdlog::get_level());
full.log_level = std::string(level_name.data(), level_name.size());
full.unshare_user = full.unshare_user.value_or(true);
full.unshare_ipc = full.unshare_ipc.value_or(true);
full.unshare_pid = full.unshare_pid.value_or(true);
full.unshare_net = full.unshare_net.value_or(true);
full.unshare_uts = full.unshare_uts.value_or(true);
full.unshare_cgroup = full.unshare_cgroup.value_or(true);
// `volumes` is left exactly as loaded -- an open-ended list with no
// "default" entry to materialize, unlike the six fixed unshare-* flags.
if (!write_config_file(config_path, full)) {
return 1;
}
fmt::print("{}\n", config_path.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,
@@ -535,6 +571,8 @@ int dispatch_command(const ParsedArgs& args, const std::filesystem::path& config
return exec_in_session(*args.exec_pid, args.command, args.user_flag, args.group_flag); return exec_in_session(*args.exec_pid, args.command, args.user_flag, args.group_flag);
case Mode::kill: case Mode::kill:
return kill_session(*args.kill_pid); return kill_session(*args.kill_pid);
case Mode::write_config:
return write_config_command(config_path, config);
case Mode::run: { case Mode::run: {
// 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
// user namespace to gain privilege, so the mount is already directly // user namespace to gain privilege, so the mount is already directly