Split config.yaml into global+persistent files; add -c/--config-file
config.yaml now holds only the global section (log-level, unshare-*, with-veth, with-ipv6); a new persistent.yaml holds volumes/networks. load_config_file()/write_config_file() are replaced by load_global_config()/load_persistent_config()/write_global_config()/ write_persistent_config(), each touching only their own file. -c/--config-file <path> lets one invocation use an alternate file for the global section only -- persistent.yaml is always the one fixed path, regardless of -c, so an experiment can never affect real volumes/networks (a -c file's own volumes/networks, if any, are simply never read either). A -c path that doesn't exist is a hard error, unlike the default path's existing missing-file leniency. migrate_legacy_config_if_needed() moves volumes/networks out of an old-format config.yaml into persistent.yaml on first run after upgrading, always against the fixed default paths regardless of -c. A name collision aborts the migration for that run (touching neither file) rather than risking data loss. Required reordering main() to parse CLI args before loading config (so -c's value is known first) -- ParsedArgs::log_level_flag_given tracks whether --log-level was already given so the config file's own log-level doesn't clobber it despite the reversed call order.
This commit is contained in:
@@ -15,14 +15,30 @@ kernel actually supports. See `README.md` for the human-facing overview (build/u
|
||||
status); this file stays the dense, file-by-file reference. Still early-stage.
|
||||
|
||||
Source layout (all under `src/`):
|
||||
- `main.cpp` — the CLI entry point only, and deliberately tiny (~30 lines):
|
||||
loads the config file, applies its `global.log-level` (`apply_log_level()`,
|
||||
`cli_args.h`) before CLI parsing so an explicit `--log-level` can still
|
||||
override it afterward, calls `parse_args()` (`cli_args.h`) and returns its
|
||||
exit code immediately if it gives one (covers `-h`/`-V` and every parse
|
||||
error), otherwise calls `dispatch_command()` (`commands.h`) and returns its
|
||||
result. All of the actual option-parsing and command logic that used to live
|
||||
here moved out into `cli_args.{h,cpp}`/`commands.{h,cpp}`/`self_test.{h,cpp}`
|
||||
- `main.cpp` — the CLI entry point only, and deliberately tiny (~45 lines):
|
||||
calls `parse_args()` (`cli_args.h`) first and returns its exit code
|
||||
immediately if it gives one (covers `-h`/`-V` and every parse error);
|
||||
otherwise calls `migrate_legacy_config_if_needed()` (`config_file.h`, see
|
||||
below) unconditionally, resolves the effective *global* config path
|
||||
(`args.config_file_flag` if `-c/--config-file` was given, else
|
||||
`config_file_path()` — a `-c` path that doesn't exist is a hard error here,
|
||||
unlike the default path's own missing-file leniency), loads that via
|
||||
`load_global_config()` and the separate, always-fixed
|
||||
`persistent_file_path()` via `load_persistent_config()`, and merges both
|
||||
into one `AppConfig`. **Config loading had to move to *after* `parse_args()`**
|
||||
(it used to run first, specifically so an explicit `--log-level` could
|
||||
still override the config file's own value by being applied later) since
|
||||
which file even supplies the global section now depends on `-c`, itself a
|
||||
CLI flag — this only works without breaking that precedence because
|
||||
`ParsedArgs::log_level_flag_given` (`cli_args.h`) tracks whether the CLI
|
||||
already gave `--log-level` (which still applies immediately, in
|
||||
`parse_args()`, unchanged); `main()` then only applies `config.log_level`
|
||||
via `apply_log_level()` when that's false, preserving the exact same final
|
||||
SPDLOG_LEVEL-env → config-file → `--log-level` precedence as before, just
|
||||
with the config load itself now happening later. Finally calls
|
||||
`dispatch_command()` (`commands.h`) and returns its result. All of the
|
||||
actual option-parsing and command logic that used to live here moved out
|
||||
into `cli_args.{h,cpp}`/`commands.{h,cpp}`/`self_test.{h,cpp}`
|
||||
(see below) specifically to keep this file from re-growing into a dumping
|
||||
ground as more commands (docker-compose support, etc.) get added.
|
||||
- `cli_args.{h,cpp}` — command-line parsing only, nothing else. `Mode` (the
|
||||
@@ -113,6 +129,22 @@ Source layout (all under `src/`):
|
||||
`port_forward.h`, since resolving which network a spec refers to needs
|
||||
runtime join state that doesn't exist yet at parse time), but has no
|
||||
standalone use at all: rejected post-loop unless combined with `-r`.
|
||||
`-c/--config-file <path>` reuses `'c'` (freed up when `--cleanup` dropped
|
||||
its own short form) into `ParsedArgs::config_file_flag` — a plain
|
||||
`required_argument` flag with no mode interaction at all, always available
|
||||
regardless of what command is being run, same as `--hostname`/`--user`.
|
||||
It only ever affects which file `main()` (`main.cpp`, see below) loads for
|
||||
the *global* section (`config_file.h`'s `load_global_config()`); resolving
|
||||
it, and deciding whether the resulting `AppConfig`'s own `log_level`
|
||||
should still be applied, both had to move out of this file and into
|
||||
`main()`, since they need to know about the loaded config, which
|
||||
`cli_args.cpp` itself has no dependency on otherwise. What `parse_args()`
|
||||
*does* still do, unchanged, is apply `--log-level` immediately in its own
|
||||
case (`apply_log_level()`) — but now also sets a new
|
||||
`ParsedArgs::log_level_flag_given` bool alongside it, purely so `main()`
|
||||
can tell afterward whether the CLI already provided one before deciding
|
||||
whether to also apply the config's own (see `main.cpp`'s own entry for why
|
||||
this two-flag dance is needed at all).
|
||||
- `commands.{h,cpp}` — every command's implementation, plus the dispatcher.
|
||||
`dispatch_command(args, config_path, config)` (the only externally-linked
|
||||
function; everything else in this file is `.cpp`-local) is a `switch
|
||||
@@ -211,22 +243,29 @@ Source layout (all under `src/`):
|
||||
expected, not a reason to leave a network the user explicitly asked to
|
||||
delete sitting in the config.
|
||||
`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
|
||||
`create_volume_command()`/`delete_volume_command()`'s own use of
|
||||
`write_persistent_config()` (which only ever persists `AppConfig` fields
|
||||
that are already set, into the separate `persistent.yaml`), this fills in
|
||||
*every global* field before writing via `write_global_config()` — the six
|
||||
`unshare-*` bools plus `with-veth`/`with-ipv6`, all 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).
|
||||
command line, overriding whatever the effective config's own `log-level`
|
||||
already was, since `main()`/`parse_args()` already applied it in that
|
||||
precedence order by the time this runs — see `main.cpp`'s own entry above
|
||||
for the full precedence chain, including how `-c/--config-file` fits in)
|
||||
— so a bare `-w` bootstraps a complete, fully-populated *global* config
|
||||
file for hand-editing, and `-w` combined with other flags captures their
|
||||
effective values into it. `volumes`/`networks` aren't part of `AppConfig`'s
|
||||
global-only concern from this command's point of view at all anymore —
|
||||
it never reads or writes `persistent.yaml`. `config_path` (the parameter
|
||||
this function takes) is `main()`'s already-resolved effective global path
|
||||
— the default `config.yaml`, or wherever `-c/--config-file` pointed
|
||||
instead — so `-w` combined with `-c` bootstraps a global config file at
|
||||
that custom location rather than the default. Prints the file's full path
|
||||
(`write_global_config()` 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`,
|
||||
@@ -2065,28 +2104,53 @@ Source layout (all under `src/`):
|
||||
`EINTR`-retry loop — matching this project's existing direct-POSIX style
|
||||
(`process.cpp` already retries `waitpid()` the same way) — rather than
|
||||
`<thread>`/`<chrono>` (unused anywhere else in this project).
|
||||
- `config_file.{h,cpp}` — `load_config_file()` reads and parses (via libyaml's
|
||||
document API, `<yaml.h>`) the `global`, `volumes`, and `networks` sections of
|
||||
the local YAML config file located by `config_file_path()`
|
||||
(`$XDG_CONFIG_HOME/slocker-lite/config.yaml`,
|
||||
falling back to `$HOME/.config/slocker-lite/config.yaml`). Supported `global` keys:
|
||||
`log-level`; six `unshare-<type>` keys (`unshare-user`/`unshare-ipc`/
|
||||
`unshare-pid`/`unshare-net`/`unshare-uts`/`unshare-cgroup`, one per
|
||||
`bwrap.cpp`'s own `namespace_probes` entry) controlling whether `-r/--run`
|
||||
requests each of bwrap's `--unshare-xxx` flags; and two `with-<feature>` keys
|
||||
(`with-veth`/`with-ipv6`, `AppConfig::with_veth`/`with_ipv6`) giving
|
||||
`-n/--network`'s own creation-time `veth`/`ipv6` policy (`NetworkEntry`,
|
||||
above) a persistent default, used whenever the corresponding
|
||||
`--with-veth`/`--with-ipv6` CLI flag (`cli_args.{h,cpp}`, see above) isn't
|
||||
given — every other long option is a one-shot flag, not a setting, so it
|
||||
doesn't belong in a persistent config file. All eight of these boolean keys
|
||||
share one small `.cpp`-local `BoolGlobalKey {key, field}` pairing table (two
|
||||
arrays, `unshare_keys` and `network_default_keys`, both consumed by shared
|
||||
`load_bool_keys()`/`write_bool_keys()` helpers rather than repeating the same
|
||||
find-parse-or-warn / emit-if-set loop body per group) and accept
|
||||
- `config_file.{h,cpp}` — reads/writes (via libyaml's document API,
|
||||
`<yaml.h>`) two separate local YAML files, not one: `config_file_path()`
|
||||
(`$XDG_CONFIG_HOME/slocker-lite/config.yaml`, falling back to
|
||||
`$HOME/.config/slocker-lite/config.yaml`) holds only the `global` section;
|
||||
`persistent_file_path()` (same directory, `persistent.yaml`) holds
|
||||
`volumes`/`networks` — both resolved through one shared `.cpp`-local
|
||||
`config_dir()` so the two paths can never drift relative to each other.
|
||||
**Split specifically so `-c/--config-file` (`cli_args.{h,cpp}`) can safely
|
||||
redirect just the global section for one invocation**: `volumes`/`networks`
|
||||
are real, provisioned host state (named volumes, live bridges/namespaces),
|
||||
and letting `-c` touch them too would risk a mistake shadowing or
|
||||
corrupting real persistent state. `load_global_config(path)` parses only
|
||||
`path`'s `global` mapping (any `volumes`/`networks` physically present are
|
||||
never read at all — this is what makes `-c` safe even against an
|
||||
old-format file that still has them); `load_persistent_config(path)` is
|
||||
the mirror image, reading only `volumes`/`networks` and ignoring `global`.
|
||||
Both share one `.cpp`-local `parse_yaml_file(path)` (open + `yaml_parser_load()`;
|
||||
a missing file yields an empty, root-less document rather than `nullopt`,
|
||||
so "doesn't exist" and "exists but empty" are indistinguishable to callers
|
||||
— both already read back as "nothing set"; `nullopt` only for a genuine
|
||||
parse failure) instead of duplicating that scaffolding twice.
|
||||
`write_global_config(path, config)`/`write_persistent_config(path, config)`
|
||||
are the write-side mirror, sharing a `.cpp`-local `write_yaml_document(path,
|
||||
document)` for the create-directory/open-file/emitter dance — carefully
|
||||
preserving the exact document-ownership rule libyaml requires
|
||||
(`yaml_emitter_dump()` consumes/deletes the document itself once
|
||||
`yaml_emitter_open()` succeeds; a failure before that point means this
|
||||
function must delete it explicitly instead, exactly as the original single
|
||||
combined write function already had to).
|
||||
Supported `global` keys: `log-level`; six `unshare-<type>` keys
|
||||
(`unshare-user`/`unshare-ipc`/`unshare-pid`/`unshare-net`/`unshare-uts`/
|
||||
`unshare-cgroup`, one per `bwrap.cpp`'s own `namespace_probes` entry)
|
||||
controlling whether `-r/--run` requests each of bwrap's `--unshare-xxx`
|
||||
flags; and two `with-<feature>` keys (`with-veth`/`with-ipv6`,
|
||||
`AppConfig::with_veth`/`with_ipv6`) giving `-n/--network`'s own
|
||||
creation-time `veth`/`ipv6` policy (`NetworkEntry`, above) a persistent
|
||||
default, used whenever the corresponding `--with-veth`/`--with-ipv6` CLI
|
||||
flag (`cli_args.{h,cpp}`, see above) isn't given — every other long option
|
||||
is a one-shot flag, not a setting, so it doesn't belong in a persistent
|
||||
config file. All eight of these boolean keys share one small `.cpp`-local
|
||||
`BoolGlobalKey {key, field}` pairing table (two arrays, `unshare_keys` and
|
||||
`network_default_keys`, both consumed by shared
|
||||
`load_bool_keys()`/`write_bool_keys()` helpers rather than repeating the
|
||||
same find-parse-or-warn / emit-if-set loop body per group) and accept
|
||||
`"1"`/`"on"`/`"yes"`/`"true"` (enabled) or `"0"`/`"off"`/`"no"`/`"false"`
|
||||
(disabled), case-insensitively — `parse_bool_flag()`, exported (not just this
|
||||
file's own internal helper) specifically so `cli_args.cpp`'s own
|
||||
(disabled), case-insensitively — `parse_bool_flag()`, exported (not just
|
||||
this file's own internal helper) specifically so `cli_args.cpp`'s own
|
||||
`--with-ipv6`/`--with-veth` value parsing accepts exactly the same forms as
|
||||
the config file itself, rather than a second, drifting copy. An unset key
|
||||
defaults to enabled, and an unrecognized value logs a `spdlog::warn` and is
|
||||
@@ -2096,16 +2160,17 @@ Source layout (all under `src/`):
|
||||
returns a default-constructed (empty) `AppConfig`, not an error; unknown
|
||||
sections/keys (and malformed individual volume entries) are likewise ignored
|
||||
for forward-compatibility.
|
||||
`main()` applies `config->log_level` (via the existing `apply_log_level()`)
|
||||
right after `spdlog::cfg::load_env_levels()` and before parsing CLI options,
|
||||
so an explicit `--log-level` on the command line always 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()`, `commands.cpp`) to persist a new
|
||||
`VolumeEntry {name, directory}` into the `volumes` section, preserving
|
||||
`global` (including any set `unshare-*`/`with-veth`/`with-ipv6` keys,
|
||||
re-serialized as canonical `"true"`/`"false"`) untouched. **`VolumeEntry`/the `volumes` section is a
|
||||
`main()` (see above) applies the merged `AppConfig`'s own `log_level` (via
|
||||
the existing `apply_log_level()`) once loaded, but only when
|
||||
`!args.log_level_flag_given` — see `main.cpp`'s own entry for why config
|
||||
loading had to move to *after* `parse_args()` (to know `-c`'s value first)
|
||||
and what that meant for preserving log-level precedence.
|
||||
`write_persistent_config()` writes `volumes`/`networks` back out — used by
|
||||
`-v/--volume` (`create_volume_command()`, `commands.cpp`, which now calls
|
||||
`persistent_file_path()` directly rather than taking a `config_path`
|
||||
parameter it no longer needs) to persist a new `VolumeEntry {name,
|
||||
directory}` into the `volumes` section, leaving `config.yaml` completely
|
||||
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).
|
||||
@@ -2128,15 +2193,42 @@ Source layout (all under `src/`):
|
||||
on load, same forward-compatible policy as everything else here. `ipv6`
|
||||
reuses `parse_bool_flag()`, defaulting to `true` (enabled) if absent or
|
||||
unparseable; `subnet6` is only read/written when `ipv6` is true.
|
||||
`write_config_file()` writes each network as its own nested mapping under
|
||||
`networks`, `ipv6` re-serialized as canonical `"true"`/`"false"` like the
|
||||
`unshare-*` keys. `veth` (default `true`) round-trips the same way as
|
||||
`write_persistent_config()` writes each network as its own nested mapping
|
||||
under `networks`, `ipv6` re-serialized as canonical `"true"`/`"false"` like
|
||||
the `unshare-*` keys. `veth` (default `true`) round-trips the same way as
|
||||
`ipv6` (`parse_bool_flag()`, written as canonical `"true"`/`"false"`,
|
||||
always written regardless of value — unlike `subnet6`, there's no
|
||||
companion field whose presence depends on it) — see `network_bridge.h`'s
|
||||
`probe_veth_support()`/`should_use_veth()` above and `--with-veth`/
|
||||
`global.with-veth` (`cli_args.{h,cpp}`/`config_file.{h,cpp}`) below for
|
||||
what it controls.
|
||||
what it controls. `create_network_command()`/`delete_network_command()`
|
||||
(`commands.cpp`) likewise call `persistent_file_path()` directly now,
|
||||
dropping the `config_path` parameter they used to take.
|
||||
|
||||
**`migrate_legacy_config_if_needed()`** — the one-time-per-upgrade bridge
|
||||
from the old single-file format to the split above: reuses
|
||||
`load_persistent_config()` *against `config_file_path()`* (the global
|
||||
file's own path) to detect legacy `volumes`/`networks` still sitting
|
||||
there — that loader only ever reads those two sections, so pointing it at
|
||||
an old-format `config.yaml` naturally surfaces whatever's left, with no
|
||||
separate detection logic needed. If found, merges them into
|
||||
`persistent_file_path()` (a name collision against an already-existing
|
||||
entry there aborts the *entire* migration for this run, touching neither
|
||||
file, with a warning identifying the conflict — never silently drops or
|
||||
overwrites data; expected to be exceedingly rare, since `persistent.yaml`
|
||||
doesn't exist at all the first time this runs for a given install), then
|
||||
rewrites `config.yaml` via `write_global_config()` — which, by
|
||||
construction, never writes `volumes`/`networks` at all, completing the
|
||||
strip. Logs an info-level summary of what moved. A cheap no-op (single
|
||||
read, no writes) when `config.yaml` has no legacy data, which is the
|
||||
common case on every run after the first. **Always operates on the fixed
|
||||
default paths, regardless of `-c/--config-file`** — migration only ever
|
||||
concerns the *default* `config.yaml`, never whatever a given invocation's
|
||||
`-c` points at instead, so an alternate global-only file can never be
|
||||
mistaken for — or have its own `volumes`/`networks`, if any, migrated
|
||||
from — the real persistent config. Called unconditionally, early in
|
||||
`main()`, before resolving which file supplies that invocation's own
|
||||
global section (see `main.cpp`'s own entry above).
|
||||
- `network_subnet.{h,cpp}` — pure CIDR arithmetic backing `-n/--network`'s
|
||||
subnet allocation and `network_bridge.{h,cpp}`'s (see below) gateway-address
|
||||
computation; no kernel/`ip`/`iptables` calls of its own. `is_valid_network_name()`
|
||||
@@ -2284,7 +2376,7 @@ Build directory is `buildDir/` (already configured).
|
||||
`--user`, `--group`, `--hostname`, `--env`, `--env-file`, `-v/--volume`, `--list-volumes`, `--delete-volume`,
|
||||
`--delete-volume-full`, `-n/--network`, `--extern`, `--intern`, `--subnet`, `--with-ipv6`, `--subnet6`,
|
||||
`--with-veth`, `--list-networks`, `--delete-network`, `-p/--port-forward`, `--no-dns`, `--list-processes`, `--clean-processes`,
|
||||
`-w/--write-config`, `-t/--test [-- <catch-command-line-options>]`, `--log-level`,
|
||||
`-c/--config-file`, `-w/--write-config`, `-t/--test [-- <catch-command-line-options>]`, `--log-level`,
|
||||
`-h/--help`, `-V/--version`)
|
||||
- Run tests: `meson test -C buildDir` (the `[unit]` + safe `[integration]` categories
|
||||
only — see `self_test.{h,cpp}`'s own entry above and `README.md`'s "Testing" section
|
||||
|
||||
Reference in New Issue
Block a user