diff --git a/CLAUDE.md b/CLAUDE.md index e58dd20..6f75e97 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -115,6 +115,22 @@ Source layout (all under `src/`): already gone) — see `config_file.{h,cpp}` below for what a "volume" means here (a distinct concept from `OciImageConfig::volumes`). `dispatch_command()`'s `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 (erroring out, `ok = false`, same as a failed `--user` resolution — `bwrap` 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`, `-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`, - `--delete-volume-full`, `--list-processes`, `--clean-processes`, `-t/--test`, `--log-level`, - `-h/--help`, `-V/--version`) + `--delete-volume-full`, `--list-processes`, `--clean-processes`, `-w/--write-config`, + `-t/--test`, `--log-level`, `-h/--help`, `-V/--version`) - Run tests: `meson test -C buildDir` ## Code style diff --git a/README.md b/README.md index b34abca..ac9dc0e 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,8 @@ slocker-lite -V|--version | `--delete-volume-full ` | 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`). | | `--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 ` | Set log verbosity (`trace`, `debug`, `info`, `warn`, `error`, `critical`, `off`). | | `-h, --help` | Print usage 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 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 Image layers are imported into `containers-storage` (parent-chained) and the diff --git a/src/cli_args.cpp b/src/cli_args.cpp index 4132214..9e07772 100644 --- a/src/cli_args.cpp +++ b/src/cli_args.cpp @@ -50,7 +50,7 @@ constexpr int env_file = 266; constexpr int kill = 267; } // namespace options -constexpr std::array long_options = {{ +constexpr std::array long_options = {{ {"help", no_argument, nullptr, 'h'}, {"version", no_argument, nullptr, 'V'}, {"test", no_argument, nullptr, 't'}, @@ -76,6 +76,7 @@ constexpr std::array long_options = {{ {"clean-processes", no_argument, nullptr, options::clean_processes}, {"env", required_argument, nullptr, options::env}, {"env-file", required_argument, nullptr, options::env_file}, + {"write-config", no_argument, nullptr, 'w'}, {nullptr, 0, nullptr, 0}, }}; @@ -95,6 +96,7 @@ void print_usage(const char* prog) { " {0} --delete-volume-full \n" " {0} --list-processes\n" " {0} --clean-processes\n" + " {0} -w|--write-config\n" " {0} -t|--test\n" " {0} -h|--help\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" " left behind by sessions that are no longer\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" " --log-level set log verbosity (trace, debug, info, warn,\n" " error, critical, off)\n" @@ -240,7 +247,7 @@ bool apply_log_level(std::string_view name) { std::optional parse_args(int argc, char* argv[], ParsedArgs& out) { opterr = 0; 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) { case 'h': print_usage(argv[0]); @@ -256,6 +263,7 @@ std::optional parse_args(int argc, char* argv[], ParsedArgs& out) { case 'l': case 'i': case 'x': + case 'w': case options::list_volumes: case options::delete_volume: case options::delete_volume_full: @@ -267,6 +275,9 @@ std::optional parse_args(int argc, char* argv[], ParsedArgs& out) { case 't': requested = Mode::test; break; + case 'w': + requested = Mode::write_config; + break; case 'm': requested = Mode::mount; break; diff --git a/src/cli_args.h b/src/cli_args.h index 368c952..7d1f279 100644 --- a/src/cli_args.h +++ b/src/cli_args.h @@ -42,7 +42,8 @@ enum class Mode { list_processes, clean_processes, exec, - kill + kill, + write_config }; // Everything parse_args() extracts from argv, ready to hand to diff --git a/src/commands.cpp b/src/commands.cpp index 9b86a79..e060ab1 100644 --- a/src/commands.cpp +++ b/src/commands.cpp @@ -366,6 +366,42 @@ int delete_volume_command(const std::string& name, const std::filesystem::path& 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, const std::vector& requested_command, bool use_nsenter, const std::optional& user, const std::optional& 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); case Mode::kill: return kill_session(*args.kill_pid); + case Mode::write_config: + return write_config_command(config_path, config); case Mode::run: { // As root, containers-storage mount doesn't need to reexec into a private // user namespace to gain privilege, so the mount is already directly