Split main.cpp into cli_args, commands, and self_test
main.cpp had grown to ~910 lines holding CLI parsing, every command
implementation, and the -t/--test handler all in one file. Split it:
- cli_args.{h,cpp}: getopt_long parsing and all post-loop validation
(parse_args()), producing a ParsedArgs the rest of the program consumes.
- commands.{h,cpp}: every command implementation plus dispatch_command(),
an exhaustive switch over Mode with no default -- so -Wswitch (this
project's warning_level=3) now catches a future Mode value added without
a matching dispatch case, instead of silently falling through. Confirmed
by temporarily adding an unhandled enumerator and observing the warning.
- self_test.{h,cpp}: -t/--test's own file, ahead of real tests landing here.
Also fixes Mode::mount, which previously had no explicit dispatch check at
all -- it ran only because it was whatever fell off the end of main()'s
if/else chain when nothing else matched. It's now mount_command(), a real
case in dispatch_command() like every other mode.
main.cpp itself shrinks to ~30 lines: load config, apply its log level,
parse_args(), dispatch_command().
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
@@ -15,42 +15,83 @@ 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.
|
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` — the CLI entry point only, and deliberately tiny (~30 lines):
|
||||||
`run_container()`, `cleanup_image()`, `unmount_image()`, `list_images_command()`,
|
loads the config file, applies its `global.log-level` (`apply_log_level()`,
|
||||||
`inspect_image_command()`, `create_volume_command()`, `list_volumes_command()`,
|
`cli_args.h`) before CLI parsing so an explicit `--log-level` can still
|
||||||
`delete_volume_command()`, `list_processes_command()`, `clean_processes_command()`).
|
override it afterward, calls `parse_args()` (`cli_args.h`) and returns its
|
||||||
`list_processes_command()` implements `--list-processes` (long-option only):
|
exit code immediately if it gives one (covers `-h`/`-V` and every parse
|
||||||
calls `list_sessions()` (`pid_file.{h,cpp}`, see below) and prints one
|
error), otherwise calls `dispatch_command()` (`commands.h`) and returns its
|
||||||
tab-aligned `pid`, `container name`, `running`/`exited` row per entry (same
|
result. All of the actual option-parsing and command logic that used to live
|
||||||
two-column tab-alignment scheme as `list_images_command()`/
|
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
|
||||||
|
enum of every CLI action) and `ParsedArgs` (everything `parse_args()`
|
||||||
|
extracts from `argv`) live in the header since `commands.h`'s
|
||||||
|
`dispatch_command()` consumes them; `print_usage()`/`print_version()`, the
|
||||||
|
`options` namespace of getopt long-option codes, and the `long_options`
|
||||||
|
array itself are `.cpp`-local. `parse_args(argc, argv, out)` runs the
|
||||||
|
`getopt_long` loop plus all of the post-loop validation that used to live at
|
||||||
|
the top of `main()`: mode/`--volume` interaction (`-v` alone vs. combined
|
||||||
|
with `-r`, see below), `--group` requires `--user`, no leftover positional
|
||||||
|
args outside `-r`/`-e`, and (moved here from what used to be inline in the
|
||||||
|
`Mode::exec` dispatch arm) `-e/--exec <pid>`'s own pid parsing/validation
|
||||||
|
(`ParsedArgs::exec_pid`, a positive integer or a hard error) and its
|
||||||
|
trailing-command requirement (`ParsedArgs::command`, required non-empty).
|
||||||
|
Returns an exit code `main()` should return immediately (`0` for `-h`/`-V`,
|
||||||
|
`1` for any parse error) when set; `nullopt` means `out` is ready for
|
||||||
|
`dispatch_command()`. `-D/--daemonize` (has a short form; `'D'` was free) is
|
||||||
|
a plain boolean flag (`ParsedArgs::daemonize_flag`, set in its own `case
|
||||||
|
'D':`, same pattern as `-n/--no-nsenter`). `--hostname <name>`/`--env
|
||||||
|
VAR=VALUE`/`--env-file <file>` (all long-option only, `--env`/`--env-file`
|
||||||
|
both repeatable) are collected here into `ParsedArgs::hostname_flag`/
|
||||||
|
`env_specs` — `--env` pushes `{false, optarg}`, `--env-file` pushes `{true,
|
||||||
|
optarg}` into the *same* ordered `std::vector<EnvSpec>` (not two separate
|
||||||
|
lists), preserving their exact relative command-line order across both
|
||||||
|
flags, since `resolve_env_specs()` (`env_spec.{h,cpp}`, see below) needs
|
||||||
|
that order to let a later one override an earlier one for the same variable
|
||||||
|
name — actually resolving them happens later, in `commands.cpp`'s
|
||||||
|
`run_container()`. `-v/--volume` is dual-purpose: used alone it's a
|
||||||
|
standalone `Mode::volume` request; combined with `-r/--run` it's repeatable
|
||||||
|
and requests a volume mount instead (resolved later by
|
||||||
|
`resolve_volume_mount()`, `volume_mount.{h,cpp}`, see below). Since `-v`
|
||||||
|
must be repeatable with `-r` but each occurrence still takes two
|
||||||
|
space-separated tokens, the getopt loop doesn't let `'v'` set `Mode`
|
||||||
|
directly: it accumulates `(spec, path)` pairs into `ParsedArgs::volume_specs`
|
||||||
|
(consuming the second token manually, with a guard against swallowing the
|
||||||
|
next flag if there isn't one), and only *after* the loop decides whether
|
||||||
|
that means one standalone `Mode::volume` call or, together with `-r`,
|
||||||
|
passes `volume_specs` through unresolved for `dispatch_command()`/
|
||||||
|
`run_container()` to handle.
|
||||||
|
- `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
|
||||||
|
(args.mode)` with **one explicit `case` per `Mode` enumerator and no
|
||||||
|
`default:`**, so `-Wswitch` (this project builds at `warning_level=3`)
|
||||||
|
forces a compile warning/error if a future `Mode` value is ever added
|
||||||
|
without a matching dispatch case, instead of silently falling through to
|
||||||
|
the wrong command — confirmed by testing (temporarily adding an unhandled
|
||||||
|
enumerator triggered exactly the expected `-Wswitch` warning). `Mode::mount`
|
||||||
|
has its own explicit case (`mount_command()`) for the same reason: it used
|
||||||
|
to be handled only by *falling off the end* of a long `if`/`else` chain in
|
||||||
|
`main()` with no explicit check at all — the very kind of implicit,
|
||||||
|
easy-to-silently-break behavior this dispatcher redesign exists to close
|
||||||
|
off, especially with more `Mode` values (docker-compose support) expected
|
||||||
|
soon. `list_processes_command()` implements `--list-processes` (long-option
|
||||||
|
only): calls `list_sessions()` (`pid_file.{h,cpp}`, see below) and prints
|
||||||
|
one tab-aligned `pid`, `container name`, `running`/`exited` row per entry
|
||||||
|
(same two-column tab-alignment scheme as `list_images_command()`/
|
||||||
`list_volumes_command()`, extended to a third column), no header row, silent
|
`list_volumes_command()`, extended to a third column), no header row, silent
|
||||||
success on an empty list. `clean_processes_command()` implements
|
success on an empty list. `clean_processes_command()` implements
|
||||||
`--clean-processes` (also long-option only): calls `clean_stale_sessions()`
|
`--clean-processes` (also long-option only): calls `clean_stale_sessions()`
|
||||||
(`pid_file.{h,cpp}`) and prints one `removed stale pid file for '<name>' (pid
|
(`pid_file.{h,cpp}`) and prints one `removed stale pid file for '<name>' (pid
|
||||||
<pid>)` line per file actually removed — nothing is printed for sessions still
|
<pid>)` line per file actually removed — nothing is printed for sessions still
|
||||||
running, and an empty result (nothing stale) is silent success, same
|
running, and an empty result (nothing stale) is silent success, same
|
||||||
convention as the rest of this file's list/delete commands. `-e/--exec <pid>`
|
convention as the rest of this file's list/delete commands. `Mode::exec`'s
|
||||||
(has a short form, unlike the rest of the process-tracking flags) dispatches
|
dispatch case is a one-line call to `exec_in_session(*args.exec_pid,
|
||||||
straight to `exec_in_session()` (`exec_session.{h,cpp}`, see below): `pid`
|
args.command)` (`exec_session.{h,cpp}`, see below) — the pid/command
|
||||||
lands in `mode_arg` (parsed as a positive integer, erroring out otherwise) the
|
parsing and validation now happens in `cli_args.cpp`'s `parse_args()`
|
||||||
same way `-r`'s image path does, and the trailing command
|
instead (see above).
|
||||||
(`argv[optind:]`, required — errors out if empty) is collected the same way
|
|
||||||
`-r`'s own command is, sharing that mode's exemption from the "no leftover
|
|
||||||
positional args" check.
|
|
||||||
`-D/--daemonize` (has a short form; `'D'` was free) is a plain boolean flag
|
|
||||||
(`daemonize_flag`, set in its own `case 'D':`, same pattern as
|
|
||||||
`-n/--no-nsenter`) threaded through to `run_container()`, which computes
|
|
||||||
`container_name` *before* `mount_image()` now (moved up from right before
|
|
||||||
the `run_bwrap()` call — it only ever depended on `image_tar`, a parameter
|
|
||||||
available from the start, so this is a pure reordering) and, if daemonizing,
|
|
||||||
calls `daemonize(container_name)` (`daemonize.{h,cpp}`, see below)
|
|
||||||
immediately after: a returned value means this is the original (parent)
|
|
||||||
process (or a hard daemonize failure) — print it and `return` right away;
|
|
||||||
`nullopt` means this is the now-detached child, which falls through into the
|
|
||||||
rest of `run_container()`'s existing body completely unchanged, including
|
|
||||||
the unmount/cleanup that already runs after `run_bwrap()` returns (no
|
|
||||||
separate watcher/reaper — the daemonized child *is* what runs the whole
|
|
||||||
session, start to finish).
|
|
||||||
`inspect_image_command()` implements `-i/--inspect
|
`inspect_image_command()` implements `-i/--inspect
|
||||||
<image.tar>`: prints every `OciImageConfig` field (user/group, exposed ports, env,
|
<image.tar>`: prints every `OciImageConfig` field (user/group, exposed ports, env,
|
||||||
volumes, default command) without mounting or running the image — extend it
|
volumes, default command) without mounting or running the image — extend it
|
||||||
@@ -67,38 +108,45 @@ Source layout (all under `src/`):
|
|||||||
(also `std::filesystem::remove_all()`s the host directory — errors out before
|
(also `std::filesystem::remove_all()`s the host directory — errors out before
|
||||||
touching the config if that fails, warns instead of failing if the directory was
|
touching the config if that fails, warns instead of failing if the directory was
|
||||||
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`). `-v/--volume` is dual-purpose:
|
distinct concept from `OciImageConfig::volumes`). `dispatch_command()`'s
|
||||||
used alone it's `create_volume_command()`; combined with `-r/--run` it instead
|
`Mode::volume`/`Mode::delete_volume`/`Mode::delete_volume_full` cases call these.
|
||||||
requests a volume mount (repeatable) and is resolved by `resolve_volume_mount()`
|
`run_container()` (the `Mode::run` dispatch case) resolves each `-v` spec
|
||||||
(see `volume_mount.{h,cpp}` below) instead. Since `-v` must be repeatable with
|
(erroring out, `ok = false`, same as a failed `--user` resolution — `bwrap`
|
||||||
`-r` but each occurrence still takes two space-separated tokens, `main()`'s
|
is skipped but unmount/cleanup still runs) into a `ResolvedVolumeMount`,
|
||||||
getopt loop no longer lets `'v'` set `Mode` itself: it accumulates
|
rejecting a duplicate or non-absolute container path first, and passes the
|
||||||
`(spec, path)` pairs into `volume_specs` (consuming the second token manually,
|
resolved list to `run_bwrap()`. `--hostname <name>` is likewise threaded
|
||||||
with a guard against swallowing the next flag if there isn't one), and only
|
straight through `run_container()` into `run_bwrap()`/`build_bwrap_args()`
|
||||||
*after* the loop decides whether that means one standalone `Mode::kVolume` call
|
(`bwrap.{h,cpp}`) — see there for how/when it actually takes effect.
|
||||||
or, together with `-r`, threads `volume_specs` through to `run_container()`.
|
`run_container()` also derives a `container_name` for the session-tracking
|
||||||
`run_container()` resolves each spec (erroring out, `ok = false`, same as a
|
pid file (see `pid_file.{h,cpp}` below): `read_image_ref()`
|
||||||
failed `--user` resolution — `bwrap` is skipped but unmount/cleanup still runs)
|
(`oci_image.{h,cpp}`) applied to the single image tar being run, formatted
|
||||||
into a `ResolvedVolumeMount`, rejecting a duplicate or non-absolute container
|
as `name:tag`, falling back to the tar's own filename stem if
|
||||||
path first, and passes the resolved list to `run_bwrap()`. `--hostname <name>`
|
`read_image_ref()` can't determine one — passed through to `run_bwrap()`
|
||||||
(long-option only, no short form) is likewise threaded straight through
|
alongside everything else. `--env`/`--env-file`'s already-ordered
|
||||||
`run_container()` into `run_bwrap()`/`build_bwrap_args()` (`bwrap.{h,cpp}`) —
|
`env_specs` (see `cli_args.{h,cpp}` above) are resolved here, once, via
|
||||||
see there for how/when it actually takes effect. `run_container()` also derives
|
`resolve_env_specs()` (`env_spec.{h,cpp}`, see below) — same `ok =
|
||||||
a `container_name` for the session-tracking pid file (see `pid_file.{h,cpp}`
|
false`-on-failure pattern as volume/user resolution — and the resolved list
|
||||||
below): `read_image_ref()` (`oci_image.{h,cpp}`) applied to the single image
|
is passed to `run_bwrap()` as `extra_env`. `-D/--daemonize`'s
|
||||||
tar being run, formatted as `name:tag`, falling back to the tar's own filename
|
`daemonize_flag` is also consumed here: `run_container()` computes
|
||||||
stem if `read_image_ref()` can't determine one — passed through to
|
`container_name` *before* `mount_image()` (needed so `daemonize()` below can
|
||||||
`run_bwrap()` alongside everything else. `--env VAR=VALUE`/`--env-file <file>`
|
use the real container name for the log file from its very first line, not
|
||||||
(both long-option only, both repeatable) accumulate into a single ordered
|
just after a later rename) and, if daemonizing, calls
|
||||||
`std::vector<EnvSpec>` — `--env` pushes `{false, optarg}`, `--env-file` pushes
|
`daemonize(container_name)` (`daemonize.{h,cpp}`, see below) immediately
|
||||||
`{true, optarg}` — preserving their exact relative command-line order across
|
after: a returned value means this is the original (parent) process (or a
|
||||||
*both* flags (not two separate lists), since `resolve_env_specs()`
|
hard daemonize failure) — print it and `return` right away; `nullopt` means
|
||||||
(`env_spec.{h,cpp}`, see below) needs that order to let a later one override
|
this is the now-detached child, which falls through into the rest of
|
||||||
an earlier one for the same variable name. `run_container()` calls it once
|
`run_container()`'s existing body completely unchanged, including the
|
||||||
(same `ok = false`-on-failure pattern as volume/user resolution) and passes
|
unmount/cleanup that already runs after `run_bwrap()` returns (no separate
|
||||||
the resolved list to `run_bwrap()` as `extra_env`.
|
watcher/reaper — the daemonized child *is* what runs the whole session,
|
||||||
|
start to finish).
|
||||||
|
- `self_test.{h,cpp}` — `run_self_tests()` implements `-t/--test`, this
|
||||||
|
project's own built-in self-test mode (distinct from the Meson-driven
|
||||||
|
fixture smoke test under `tests/`, described in "Build & test commands"
|
||||||
|
below). Currently just reports `detect_bwrap_unshare_args()`'s output
|
||||||
|
(`bwrap.{h,cpp}`); deliberately its own small file since real tests are
|
||||||
|
expected here soon.
|
||||||
- `env_spec.{h,cpp}` — `resolve_env_specs()` turns an ordered list of
|
- `env_spec.{h,cpp}` — `resolve_env_specs()` turns an ordered list of
|
||||||
`EnvSpec {is_file, value}` (see `main.cpp` above) into a flat, ordered list of
|
`EnvSpec {is_file, value}` (see `cli_args.{h,cpp}` above) into a flat, ordered list of
|
||||||
`(key, value)` pairs. A literal (`--env`) is split at its *first* `=` (the
|
`(key, value)` pairs. A literal (`--env`) is split at its *first* `=` (the
|
||||||
value may itself contain `=`; the key must be non-empty). A file (`--env-file`)
|
value may itself contain `=`; the key must be non-empty). A file (`--env-file`)
|
||||||
is read line by line: blank/whitespace-only lines and lines whose first
|
is read line by line: blank/whitespace-only lines and lines whose first
|
||||||
@@ -120,14 +168,14 @@ Source layout (all under `src/`):
|
|||||||
annotations (`io.containerd.image.name` preferred, else
|
annotations (`io.containerd.image.name` preferred, else
|
||||||
`org.opencontainers.image.ref.name`), falling back to the archive's filename and
|
`org.opencontainers.image.ref.name`), falling back to the archive's filename and
|
||||||
`"latest"` respectively. `read_image_ref()` is public (not just an internal
|
`"latest"` respectively. `read_image_ref()` is public (not just an internal
|
||||||
helper of `list_oci_images()`) precisely so `run_container()` (`main.cpp`) can
|
helper of `list_oci_images()`) precisely so `run_container()` (`commands.cpp`) can
|
||||||
reuse the exact same logic to name a *single* image tar's session pid file (see
|
reuse the exact same logic to name a *single* image tar's session pid file (see
|
||||||
`pid_file.{h,cpp}` below) instead of duplicating it.
|
`pid_file.{h,cpp}` below) instead of duplicating it.
|
||||||
`read_oci_image_config()` reads the image config blob referenced by the manifest
|
`read_oci_image_config()` reads the image config blob referenced by the manifest
|
||||||
and extracts `User` (split on `:` into `OciImageConfig::user`/`group`),
|
and extracts `User` (split on `:` into `OciImageConfig::user`/`group`),
|
||||||
`ExposedPorts`, `Env`, `Volumes`, and the effective default command
|
`ExposedPorts`, `Env`, `Volumes`, and the effective default command
|
||||||
(`Entrypoint ++ Cmd`). `user`/`group` and the default command are consumed by
|
(`Entrypoint ++ Cmd`). `user`/`group` and the default command are consumed by
|
||||||
`-r/--run`, and every field is displayed by `-i/--inspect` (see `main.cpp` above)
|
`-r/--run`, and every field is displayed by `-i/--inspect` (see `commands.{h,cpp}` above)
|
||||||
— `ExposedPorts`/`Env`/`Volumes` are otherwise still just captured for when
|
— `ExposedPorts`/`Env`/`Volumes` are otherwise still just captured for when
|
||||||
networking/volumes are implemented.
|
networking/volumes are implemented.
|
||||||
- `containers_storage.{h,cpp}` — wraps the `containers-storage` CLI (`import-layer`,
|
- `containers_storage.{h,cpp}` — wraps the `containers-storage` CLI (`import-layer`,
|
||||||
@@ -235,7 +283,7 @@ Source layout (all under `src/`):
|
|||||||
`"/root"` for uid 0 or `"/"` otherwise when there's no matching row.
|
`"/root"` for uid 0 or `"/"` otherwise when there's no matching row.
|
||||||
`build_sandbox_env()` (`bwrap.cpp`) sets the sandboxed process's `HOME` from
|
`build_sandbox_env()` (`bwrap.cpp`) sets the sandboxed process's `HOME` from
|
||||||
this — `"/root"` only when no user override applies at all (no `--user`, no
|
this — `"/root"` only when no user override applies at all (no `--user`, no
|
||||||
image-declared `config.User`). `run_container()` (`main.cpp`) calls `resolve_user_and_group()`
|
image-declared `config.User`). `run_container()` (`commands.cpp`) calls `resolve_user_and_group()`
|
||||||
with either the explicit `--user`/`--group` flags, or, when `--user` wasn't given,
|
with either the explicit `--user`/`--group` flags, or, when `--user` wasn't given,
|
||||||
the image's own declared `config.User` (`OciImageConfig::user`/`group`) — so a
|
the image's own declared `config.User` (`OciImageConfig::user`/`group`) — so a
|
||||||
container defaults to running as whatever user the image itself declares, not
|
container defaults to running as whatever user the image itself declares, not
|
||||||
@@ -288,7 +336,7 @@ Source layout (all under `src/`):
|
|||||||
failure path here (can't create the directory/file, can't lock, can't remove)
|
failure path here (can't create the directory/file, can't lock, can't remove)
|
||||||
is a `spdlog::warn`, never fatal — session tracking is best-effort and must
|
is a `spdlog::warn`, never fatal — session tracking is best-effort and must
|
||||||
never block or fail `-r/--run` itself. `list_sessions()` implements
|
never block or fail `-r/--run` itself. `list_sessions()` implements
|
||||||
`--list-processes` (`main.cpp`'s `list_processes_command()`): scans the same
|
`--list-processes` (`commands.cpp`'s `list_processes_command()`): scans the same
|
||||||
`run/` directory and reports one `SessionInfo {pid, container_name, running}`
|
`run/` directory and reports one `SessionInfo {pid, container_name, running}`
|
||||||
per readable pid file. `pid` is read from the file's own contents, not parsed
|
per readable pid file. `pid` is read from the file's own contents, not parsed
|
||||||
from the filename (ambiguous for names that themselves contain `-`);
|
from the filename (ambiguous for names that themselves contain `-`);
|
||||||
@@ -336,7 +384,7 @@ Source layout (all under `src/`):
|
|||||||
`--daemonize` was asked to capture would defeat the point of the flag. The
|
`--daemonize` was asked to capture would defeat the point of the flag. The
|
||||||
child reports `"LOG <path>\n"` over the pipe immediately (so the parent can
|
child reports `"LOG <path>\n"` over the pipe immediately (so the parent can
|
||||||
show a useful location even on failure) and returns `nullopt` to its caller
|
show a useful location even on failure) and returns `nullopt` to its caller
|
||||||
(`run_container()`, `main.cpp`), which then falls through into the rest of
|
(`run_container()`, `commands.cpp`), which then falls through into the rest of
|
||||||
that function's existing body completely unchanged — **the daemonized child
|
that function's existing body completely unchanged — **the daemonized child
|
||||||
is what runs the whole rest of `run_container()`, including the unmount/
|
is what runs the whole rest of `run_container()`, including the unmount/
|
||||||
cleanup that already existed after `run_bwrap()` returns; no separate
|
cleanup that already existed after `run_bwrap()` returns; no separate
|
||||||
@@ -410,7 +458,7 @@ Source layout (all under `src/`):
|
|||||||
overwrites it afterward — same precedence 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
|
`write_config_file()` writes the whole file back out (via libyaml's
|
||||||
document-building/emitter API, symmetric to the read side) — used by
|
document-building/emitter API, symmetric to the read side) — used by
|
||||||
`-v/--volume` (`create_volume_command()`, `main.cpp`) to persist a new
|
`-v/--volume` (`create_volume_command()`, `commands.cpp`) to persist a new
|
||||||
`VolumeEntry {name, directory}` into the `volumes` section, preserving `global`
|
`VolumeEntry {name, directory}` into the `volumes` section, preserving `global`
|
||||||
untouched. **`VolumeEntry`/the `volumes` section is a distinct concept from
|
untouched. **`VolumeEntry`/the `volumes` section is a distinct concept from
|
||||||
`OciImageConfig::volumes`**: this is a user-defined `name -> host directory`
|
`OciImageConfig::volumes`**: this is a user-defined `name -> host directory`
|
||||||
@@ -506,7 +554,7 @@ Build directory is `buildDir/` (already configured).
|
|||||||
the enum's own name (e.g. `Mode::run`, `OciPortProtocol::tcp`), so plain snake_case
|
the enum's own name (e.g. `Mode::run`, `OciPortProtocol::tcp`), so plain snake_case
|
||||||
enumerators are enough on their own. Free-standing constants also use plain snake_case;
|
enumerators are enough on their own. Free-standing constants also use plain snake_case;
|
||||||
when several are conceptually related, group them under a named `namespace` instead of
|
when several are conceptually related, group them under a named `namespace` instead of
|
||||||
relying on a shared prefix to imply the grouping (e.g. `main.cpp`'s `getopt_long` long-option
|
relying on a shared prefix to imply the grouping (e.g. `cli_args.cpp`'s `getopt_long` long-option
|
||||||
codes live in `namespace options { constexpr int log_level = ...; }`, and `bwrap.cpp`'s
|
codes live in `namespace options { constexpr int log_level = ...; }`, and `bwrap.cpp`'s
|
||||||
priv-drop-helper path/binary-name pair live in `namespace priv_drop { ... }`) — nest the named
|
priv-drop-helper path/binary-name pair live in `namespace priv_drop { ... }`) — nest the named
|
||||||
namespace inside the file's existing anonymous namespace where one is already present, so
|
namespace inside the file's existing anonymous namespace where one is already present, so
|
||||||
|
|||||||
+2
-1
@@ -17,7 +17,8 @@ conf_data.set10('ENABLE_TESTS', get_option('enable_tests'))
|
|||||||
configure_file(output : 'config.h', configuration : conf_data)
|
configure_file(output : 'config.h', configuration : conf_data)
|
||||||
|
|
||||||
slocker_lite = executable('slocker-lite',
|
slocker_lite = executable('slocker-lite',
|
||||||
['src/main.cpp', 'src/process.cpp', 'src/oci_image.cpp', 'src/containers_storage.cpp',
|
['src/main.cpp', 'src/cli_args.cpp', 'src/commands.cpp', 'src/self_test.cpp',
|
||||||
|
'src/process.cpp', 'src/oci_image.cpp', 'src/containers_storage.cpp',
|
||||||
'src/bwrap.cpp', 'src/user_spec.cpp', 'src/config_file.cpp', 'src/volume_mount.cpp',
|
'src/bwrap.cpp', 'src/user_spec.cpp', 'src/config_file.cpp', 'src/volume_mount.cpp',
|
||||||
'src/pid_file.cpp', 'src/exec_session.cpp', 'src/env_spec.cpp', 'src/daemonize.cpp'],
|
'src/pid_file.cpp', 'src/exec_session.cpp', 'src/env_spec.cpp', 'src/daemonize.cpp'],
|
||||||
include_directories : include_directories('.'),
|
include_directories : include_directories('.'),
|
||||||
|
|||||||
@@ -0,0 +1,386 @@
|
|||||||
|
// Copyright (C) 2026 Viorel Munteanu
|
||||||
|
//
|
||||||
|
// This program is free software; you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation; either version 2 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License along
|
||||||
|
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
#include "cli_args.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <getopt.h>
|
||||||
|
|
||||||
|
#include <fmt/core.h>
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// --log-level/--user/--group/--list-volumes/--delete-volume[-full]/--hostname/
|
||||||
|
// --list-processes/--clean-processes/--env/--env-file have no short form
|
||||||
|
// (--log-level's was freed up so -l could become --list-images; -u is already
|
||||||
|
// --umount; the rest have no natural free letter left, or don't need one), so
|
||||||
|
// they need long-option vals outside the printable-char range short options use.
|
||||||
|
namespace options {
|
||||||
|
constexpr int log_level = 256;
|
||||||
|
constexpr int user = 257;
|
||||||
|
constexpr int group = 258;
|
||||||
|
constexpr int list_volumes = 259;
|
||||||
|
constexpr int delete_volume = 260;
|
||||||
|
constexpr int delete_volume_full = 261;
|
||||||
|
constexpr int hostname = 262;
|
||||||
|
constexpr int list_processes = 263;
|
||||||
|
constexpr int clean_processes = 264;
|
||||||
|
constexpr int env = 265;
|
||||||
|
constexpr int env_file = 266;
|
||||||
|
} // namespace options
|
||||||
|
|
||||||
|
constexpr std::array<struct option, 25> long_options = {{
|
||||||
|
{"help", no_argument, nullptr, 'h'},
|
||||||
|
{"version", no_argument, nullptr, 'V'},
|
||||||
|
{"test", no_argument, nullptr, 't'},
|
||||||
|
{"log-level", required_argument, nullptr, options::log_level},
|
||||||
|
{"mount", required_argument, nullptr, 'm'},
|
||||||
|
{"umount", required_argument, nullptr, 'u'},
|
||||||
|
{"run", required_argument, nullptr, 'r'},
|
||||||
|
{"cleanup", required_argument, nullptr, 'c'},
|
||||||
|
{"no-nsenter", no_argument, nullptr, 'n'},
|
||||||
|
{"daemonize", no_argument, nullptr, 'D'},
|
||||||
|
{"list-images", required_argument, nullptr, 'l'},
|
||||||
|
{"user", required_argument, nullptr, options::user},
|
||||||
|
{"group", required_argument, nullptr, options::group},
|
||||||
|
{"volume", required_argument, nullptr, 'v'},
|
||||||
|
{"list-volumes", no_argument, nullptr, options::list_volumes},
|
||||||
|
{"delete-volume", required_argument, nullptr, options::delete_volume},
|
||||||
|
{"delete-volume-full", required_argument, nullptr, options::delete_volume_full},
|
||||||
|
{"inspect", required_argument, nullptr, 'i'},
|
||||||
|
{"exec", required_argument, nullptr, 'e'},
|
||||||
|
{"hostname", required_argument, nullptr, options::hostname},
|
||||||
|
{"list-processes", no_argument, nullptr, options::list_processes},
|
||||||
|
{"clean-processes", no_argument, nullptr, options::clean_processes},
|
||||||
|
{"env", required_argument, nullptr, options::env},
|
||||||
|
{"env-file", required_argument, nullptr, options::env_file},
|
||||||
|
{nullptr, 0, nullptr, 0},
|
||||||
|
}};
|
||||||
|
|
||||||
|
void print_usage(const char* prog) {
|
||||||
|
fmt::print(
|
||||||
|
"usage: {0} -m|--mount <image.tar>\n"
|
||||||
|
" {0} -r|--run <image.tar> [-v <name-or-dir> <container-path>]... [-- <command> [args...]]\n"
|
||||||
|
" {0} -u|--umount <layer-id>\n"
|
||||||
|
" {0} -c|--cleanup <layer-id>\n"
|
||||||
|
" {0} -l|--list-images <directory>\n"
|
||||||
|
" {0} -i|--inspect <image.tar>\n"
|
||||||
|
" {0} -e|--exec <pid> [-- <command> [args...]]\n"
|
||||||
|
" {0} -v|--volume <name> <directory>\n"
|
||||||
|
" {0} --list-volumes\n"
|
||||||
|
" {0} --delete-volume <name>\n"
|
||||||
|
" {0} --delete-volume-full <name>\n"
|
||||||
|
" {0} --list-processes\n"
|
||||||
|
" {0} --clean-processes\n"
|
||||||
|
" {0} -t|--test\n"
|
||||||
|
" {0} -h|--help\n"
|
||||||
|
" {0} -V|--version\n"
|
||||||
|
"\n"
|
||||||
|
"options:\n"
|
||||||
|
" -m, --mount <image.tar> validate and mount an OCI Image Layout tar\n"
|
||||||
|
" -r, --run <image.tar> mount, run bwrap in the foreground (default\n"
|
||||||
|
" command: the image's own Entrypoint/Cmd if set,\n"
|
||||||
|
" else /bin/sh; pass -- <command> [args...] to\n"
|
||||||
|
" override), then unmount and clean up when it\n"
|
||||||
|
" exits\n"
|
||||||
|
" -u, --umount <layer-id> unmount a previously mounted image layer (the\n"
|
||||||
|
" ID printed by --mount/--run, or from\n"
|
||||||
|
" `containers-storage layers`)\n"
|
||||||
|
" -c, --cleanup <layer-id> delete a layer and its ancestor chain from\n"
|
||||||
|
" local storage (unmount it first with --umount)\n"
|
||||||
|
" -n, --no-nsenter with --run, bind the mount directly instead of\n"
|
||||||
|
" nsenter-ing into fuse-overlayfs's namespace\n"
|
||||||
|
" (this is automatic when running as root, where\n"
|
||||||
|
" the mount is already directly visible; pass\n"
|
||||||
|
" this to force it off otherwise)\n"
|
||||||
|
" -D, --daemonize with --run, fork into the background: detaches\n"
|
||||||
|
" from the controlling terminal (setsid()) and\n"
|
||||||
|
" ignores SIGHUP, redirecting stdin from /dev/null\n"
|
||||||
|
" and stdout/stderr to a log file under\n"
|
||||||
|
" $XDG_STATE_HOME/slocker-lite/logs/. Prints the\n"
|
||||||
|
" session's pid and log path, then returns\n"
|
||||||
|
" immediately -- the same pid --list-processes/\n"
|
||||||
|
" -e/--exec use\n"
|
||||||
|
" --user <user> with --run, run the command as this user (name\n"
|
||||||
|
" or numeric uid) instead of the image's own\n"
|
||||||
|
" declared user (or root, if it declares none),\n"
|
||||||
|
" resolved against the image's own /etc/passwd;\n"
|
||||||
|
" only takes effect when --run executes as root\n"
|
||||||
|
" (no user namespace involved)\n"
|
||||||
|
" --group <group> with --user, use this group (name or numeric\n"
|
||||||
|
" gid) instead of the user's own primary group\n"
|
||||||
|
" --hostname <name> with --run, set the sandbox's hostname (only\n"
|
||||||
|
" takes effect if the running kernel supports\n"
|
||||||
|
" --unshare-uts; otherwise ignored with a\n"
|
||||||
|
" warning)\n"
|
||||||
|
" --env VAR=VALUE with --run, set an environment variable in the\n"
|
||||||
|
" sandbox (overrides the default PATH/HOME/PWD/\n"
|
||||||
|
" TERM if given the same name). May be repeated;\n"
|
||||||
|
" combined with --env-file in command-line order,\n"
|
||||||
|
" each later one winning over an earlier one for\n"
|
||||||
|
" the same name\n"
|
||||||
|
" --env-file <file> with --run, load environment variables from\n"
|
||||||
|
" <file> (one VAR=VALUE per line; blank lines and\n"
|
||||||
|
" #-comments are skipped). May be repeated\n"
|
||||||
|
" -l, --list-images <dir> list OCI Image Layout tars (*.tar, *.tar.*) found\n"
|
||||||
|
" directly in <dir>, with their name:tag\n"
|
||||||
|
" -i, --inspect <image.tar> print an image's declared user, exposed ports,\n"
|
||||||
|
" env, volumes, and default command, without\n"
|
||||||
|
" mounting or running it\n"
|
||||||
|
" -e, --exec <pid> join a running --run session (pid must be one\n"
|
||||||
|
" shown by --list-processes as \"running\") and run\n"
|
||||||
|
" a command inside its container; pass\n"
|
||||||
|
" -- <command> [args...] to specify it\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. With --run, instead\n"
|
||||||
|
" mount a volume into the sandbox: <name> is a\n"
|
||||||
|
" named volume or, if it contains '/', a host\n"
|
||||||
|
" directory (created if missing); <dir> is the\n"
|
||||||
|
" absolute path inside the container to mount it\n"
|
||||||
|
" at. May be repeated with --run. If the host\n"
|
||||||
|
" directory is empty and the image already has\n"
|
||||||
|
" content there, it's copied in first\n"
|
||||||
|
" --list-volumes list all named volumes (see -v/--volume) with\n"
|
||||||
|
" their host directory\n"
|
||||||
|
" --delete-volume <name>\n"
|
||||||
|
" remove a named volume from the config (the host\n"
|
||||||
|
" directory is left untouched)\n"
|
||||||
|
" --delete-volume-full <name>\n"
|
||||||
|
" like --delete-volume, but also recursively\n"
|
||||||
|
" deletes the volume's host directory\n"
|
||||||
|
" --list-processes list running --run sessions found by their pid\n"
|
||||||
|
" files under $XDG_STATE_HOME/slocker-lite/run/,\n"
|
||||||
|
" with their pid, container name, and status\n"
|
||||||
|
" (running or exited)\n"
|
||||||
|
" --clean-processes remove stale pid files (see --list-processes)\n"
|
||||||
|
" left behind by sessions that are no longer\n"
|
||||||
|
" running\n"
|
||||||
|
" -t, --test run the test suite\n"
|
||||||
|
" --log-level <level> set log verbosity (trace, debug, info, warn,\n"
|
||||||
|
" error, critical, off)\n"
|
||||||
|
" -h, --help print this help and exit\n"
|
||||||
|
" -V, --version print version information and exit\n",
|
||||||
|
prog);
|
||||||
|
}
|
||||||
|
|
||||||
|
void print_version() {
|
||||||
|
fmt::print(
|
||||||
|
"{} {}\n"
|
||||||
|
"Licensed under GNU GPL version 2 or later <https://gnu.org/licenses/gpl-2.0.html>\n"
|
||||||
|
"This is free software: you are free to change and redistribute it.\n"
|
||||||
|
"There is NO WARRANTY, to the extent permitted by law.\n",
|
||||||
|
PACKAGE, VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool apply_log_level(std::string_view name) {
|
||||||
|
constexpr std::array<std::string_view, 7> valid_levels = {
|
||||||
|
"trace", "debug", "info", "warn", "error", "critical", "off"};
|
||||||
|
for (auto level : valid_levels) {
|
||||||
|
if (level == name) {
|
||||||
|
spdlog::set_level(spdlog::level::from_str(std::string(name)));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spdlog::error("invalid log level: {}", name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<int> 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:e:D", long_options.data(), nullptr)) != -1) {
|
||||||
|
switch (opt) {
|
||||||
|
case 'h':
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 0;
|
||||||
|
case 'V':
|
||||||
|
print_version();
|
||||||
|
return 0;
|
||||||
|
case 't':
|
||||||
|
case 'm':
|
||||||
|
case 'u':
|
||||||
|
case 'r':
|
||||||
|
case 'c':
|
||||||
|
case 'l':
|
||||||
|
case 'i':
|
||||||
|
case 'e':
|
||||||
|
case options::list_volumes:
|
||||||
|
case options::delete_volume:
|
||||||
|
case options::delete_volume_full:
|
||||||
|
case options::list_processes:
|
||||||
|
case options::clean_processes: {
|
||||||
|
Mode requested;
|
||||||
|
switch (opt) {
|
||||||
|
case 't':
|
||||||
|
requested = Mode::test;
|
||||||
|
break;
|
||||||
|
case 'm':
|
||||||
|
requested = Mode::mount;
|
||||||
|
break;
|
||||||
|
case 'u':
|
||||||
|
requested = Mode::unmount;
|
||||||
|
break;
|
||||||
|
case 'r':
|
||||||
|
requested = Mode::run;
|
||||||
|
break;
|
||||||
|
case 'c':
|
||||||
|
requested = Mode::cleanup;
|
||||||
|
break;
|
||||||
|
case 'l':
|
||||||
|
requested = Mode::list_images;
|
||||||
|
break;
|
||||||
|
case 'i':
|
||||||
|
requested = Mode::inspect;
|
||||||
|
break;
|
||||||
|
case 'e':
|
||||||
|
requested = Mode::exec;
|
||||||
|
break;
|
||||||
|
case options::list_volumes:
|
||||||
|
requested = Mode::list_volumes;
|
||||||
|
break;
|
||||||
|
case options::delete_volume:
|
||||||
|
requested = Mode::delete_volume;
|
||||||
|
break;
|
||||||
|
case options::delete_volume_full:
|
||||||
|
requested = Mode::delete_volume_full;
|
||||||
|
break;
|
||||||
|
case options::list_processes:
|
||||||
|
requested = Mode::list_processes;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
requested = Mode::clean_processes;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (out.mode != Mode::none && out.mode != requested) {
|
||||||
|
spdlog::error("multiple actions specified");
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
out.mode = requested;
|
||||||
|
if (optarg) {
|
||||||
|
out.mode_arg = optarg;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'v': {
|
||||||
|
// -v/--volume takes two tokens: optarg (the name or host path) plus
|
||||||
|
// the immediately following argv entry (the directory, or, with -r,
|
||||||
|
// the container path). Consumed manually (rather than via getopt's
|
||||||
|
// own required_argument) so -v can repeat with -r -- see below for
|
||||||
|
// how the two uses are told apart.
|
||||||
|
if (optind >= argc || (argv[optind][0] == '-' && argv[optind][1] != '\0')) {
|
||||||
|
spdlog::error("--volume requires a name/path and a directory or container path");
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
out.volume_specs.emplace_back(optarg, argv[optind]);
|
||||||
|
++optind;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'n':
|
||||||
|
out.disable_nsenter = true;
|
||||||
|
break;
|
||||||
|
case 'D':
|
||||||
|
out.daemonize_flag = true;
|
||||||
|
break;
|
||||||
|
case options::log_level:
|
||||||
|
if (!apply_log_level(optarg)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case options::user:
|
||||||
|
out.user_flag = optarg;
|
||||||
|
break;
|
||||||
|
case options::group:
|
||||||
|
out.group_flag = optarg;
|
||||||
|
break;
|
||||||
|
case options::hostname:
|
||||||
|
out.hostname_flag = optarg;
|
||||||
|
break;
|
||||||
|
case options::env:
|
||||||
|
out.env_specs.push_back({false, optarg});
|
||||||
|
break;
|
||||||
|
case options::env_file:
|
||||||
|
out.env_specs.push_back({true, optarg});
|
||||||
|
break;
|
||||||
|
case ':':
|
||||||
|
spdlog::error("option requires an argument: -{}", static_cast<char>(optopt));
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
case '?':
|
||||||
|
default:
|
||||||
|
spdlog::error("unrecognized option");
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!out.volume_specs.empty() && out.mode != Mode::run) {
|
||||||
|
if (out.mode != Mode::none) {
|
||||||
|
spdlog::error("--volume can only be used standalone or together with --run");
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (out.volume_specs.size() > 1) {
|
||||||
|
spdlog::error("--volume can only be used once outside of --run");
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
out.mode = Mode::volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out.mode == Mode::none) {
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (out.mode != Mode::run && out.mode != Mode::exec && optind != argc) {
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (out.group_flag && !out.user_flag) {
|
||||||
|
spdlog::error("--group requires --user");
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out.mode == Mode::run || out.mode == Mode::exec) {
|
||||||
|
out.command.assign(argv + optind, argv + argc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out.mode == Mode::exec) {
|
||||||
|
if (out.command.empty()) {
|
||||||
|
spdlog::error("--exec requires a command to run");
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
char* end = nullptr;
|
||||||
|
long parsed = std::strtol(out.mode_arg.c_str(), &end, 10);
|
||||||
|
if (out.mode_arg.empty() || !end || *end != '\0' || parsed <= 0) {
|
||||||
|
spdlog::error("invalid pid: {}", out.mode_arg);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
out.exec_pid = static_cast<pid_t>(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// Copyright (C) 2026 Viorel Munteanu
|
||||||
|
//
|
||||||
|
// This program is free software; you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation; either version 2 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License along
|
||||||
|
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
#include "env_spec.h"
|
||||||
|
|
||||||
|
enum class Mode {
|
||||||
|
none,
|
||||||
|
mount,
|
||||||
|
unmount,
|
||||||
|
test,
|
||||||
|
run,
|
||||||
|
cleanup,
|
||||||
|
list_images,
|
||||||
|
volume,
|
||||||
|
list_volumes,
|
||||||
|
delete_volume,
|
||||||
|
delete_volume_full,
|
||||||
|
inspect,
|
||||||
|
list_processes,
|
||||||
|
clean_processes,
|
||||||
|
exec
|
||||||
|
};
|
||||||
|
|
||||||
|
// Everything parse_args() extracts from argv, ready to hand to
|
||||||
|
// dispatch_command() (commands.h).
|
||||||
|
struct ParsedArgs {
|
||||||
|
Mode mode = Mode::none;
|
||||||
|
std::string mode_arg;
|
||||||
|
bool disable_nsenter = false;
|
||||||
|
bool daemonize_flag = false;
|
||||||
|
std::optional<std::string> user_flag;
|
||||||
|
std::optional<std::string> group_flag;
|
||||||
|
std::optional<std::string> hostname_flag;
|
||||||
|
std::vector<std::pair<std::string, std::string>> volume_specs;
|
||||||
|
std::vector<EnvSpec> env_specs;
|
||||||
|
// Trailing argv (after getopt_long stops), populated only for
|
||||||
|
// Mode::run/Mode::exec -- the command to run, or to run under -e/--exec.
|
||||||
|
std::vector<std::string> command;
|
||||||
|
// Parsed and range-validated from mode_arg when mode == Mode::exec.
|
||||||
|
std::optional<pid_t> exec_pid;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validates and applies a log-level name (trace/debug/info/warn/error/critical/off)
|
||||||
|
// via spdlog::set_level(). Returns false (logging an error) if `name` isn't
|
||||||
|
// recognized. Exposed (not parse_args()-internal) because main() also applies it
|
||||||
|
// directly for the config file's global.log-level, before parse_args() runs, so
|
||||||
|
// an explicit --log-level on the command line can still override it afterward.
|
||||||
|
bool apply_log_level(std::string_view name);
|
||||||
|
|
||||||
|
// Parses argv via getopt_long() into `out`, including all of the post-loop
|
||||||
|
// validation (mode/--volume interaction, --group requires --user, leftover
|
||||||
|
// positional args outside --run/--exec, --exec's own command/pid validation).
|
||||||
|
// Returns an exit code main() should return immediately (0 for -h/--help or
|
||||||
|
// -V/--version, 1 for any parse error) -- print_usage()/print_version() are
|
||||||
|
// already called internally in those cases. nullopt means parsing succeeded
|
||||||
|
// and `out` is ready for dispatch_command() (commands.h).
|
||||||
|
std::optional<int> parse_args(int argc, char* argv[], ParsedArgs& out);
|
||||||
@@ -0,0 +1,526 @@
|
|||||||
|
// Copyright (C) 2026 Viorel Munteanu
|
||||||
|
//
|
||||||
|
// This program is free software; you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation; either version 2 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License along
|
||||||
|
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
#include "commands.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <functional>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fmt/core.h>
|
||||||
|
#include <fmt/ranges.h>
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
#include "bwrap.h"
|
||||||
|
#include "containers_storage.h"
|
||||||
|
#include "daemonize.h"
|
||||||
|
#include "env_spec.h"
|
||||||
|
#include "exec_session.h"
|
||||||
|
#include "oci_image.h"
|
||||||
|
#include "pid_file.h"
|
||||||
|
#include "process.h"
|
||||||
|
#include "self_test.h"
|
||||||
|
#include "user_spec.h"
|
||||||
|
#include "volume_mount.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr std::array<std::string_view, 2> required_tools = {"containers-storage", "bwrap"};
|
||||||
|
|
||||||
|
// Shared by list_images_command()/list_volumes_command()/list_processes_command():
|
||||||
|
// pad each entry with tabs (not spaces) so columns line up on an 8-column tab
|
||||||
|
// stop past the longest entry in that column, however long that is.
|
||||||
|
constexpr size_t tab_width = 8;
|
||||||
|
|
||||||
|
bool check_required_dependencies() {
|
||||||
|
bool all_found = true;
|
||||||
|
for (auto name : required_tools) {
|
||||||
|
if (!find_in_path(name)) {
|
||||||
|
spdlog::error("required dependency not found in PATH: {}", name);
|
||||||
|
all_found = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return all_found;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MountedImage {
|
||||||
|
std::string merged_path;
|
||||||
|
std::string top_layer_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::optional<MountedImage> mount_image(const std::filesystem::path& image_tar) {
|
||||||
|
if (!check_required_dependencies()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto mount_program = find_in_path("fuse-overlayfs");
|
||||||
|
if (!mount_program) {
|
||||||
|
spdlog::error("fuse-overlayfs not found in PATH");
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
g_mount_program = mount_program->string();
|
||||||
|
|
||||||
|
auto layers = read_oci_layers(image_tar);
|
||||||
|
if (!layers) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string parent_id;
|
||||||
|
for (const auto& layer : *layers) {
|
||||||
|
std::filesystem::path tmp_file =
|
||||||
|
std::filesystem::temp_directory_path() /
|
||||||
|
fmt::format("slocker-lite-{}-{}", getpid(), oci_digest_hex(layer.digest));
|
||||||
|
|
||||||
|
if (!extract_blob_to_file(image_tar, oci_digest_hex(layer.digest), tmp_file)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto layer_id = import_layer(tmp_file, parent_id);
|
||||||
|
std::filesystem::remove(tmp_file);
|
||||||
|
if (!layer_id) {
|
||||||
|
spdlog::error("failed to import layer {}", layer.digest);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
parent_id = *layer_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto merged = mount_layer(parent_id);
|
||||||
|
if (!merged) {
|
||||||
|
spdlog::error("failed to mount assembled image");
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MountedImage{*merged, parent_id};
|
||||||
|
}
|
||||||
|
|
||||||
|
int mount_command(const std::filesystem::path& image_tar) {
|
||||||
|
auto mounted = mount_image(image_tar);
|
||||||
|
if (!mounted) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
fmt::print("mounted image at: {} (layer {})\n", mounted->merged_path, mounted->top_layer_id);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int unmount_image(const std::string& layer_id) {
|
||||||
|
if (!check_required_dependencies()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (!unmount_layer(layer_id)) {
|
||||||
|
spdlog::error("failed to unmount layer {}", layer_id);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
fmt::print("unmounted layer {}\n", layer_id);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cleanup_image(const std::string& layer_id) {
|
||||||
|
if (!check_required_dependencies()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (!cleanup_layer_chain(layer_id)) {
|
||||||
|
spdlog::error("failed to clean up layer {}", layer_id);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
fmt::print("cleaned up layer {}\n", layer_id);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int list_images_command(const std::filesystem::path& dir) {
|
||||||
|
auto images = list_oci_images(dir);
|
||||||
|
if (!images) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> refs;
|
||||||
|
refs.reserve(images->size());
|
||||||
|
size_t max_len = 0;
|
||||||
|
for (const auto& ref : *images) {
|
||||||
|
refs.push_back(fmt::format("{}:{}", ref.name, ref.tag));
|
||||||
|
max_len = std::max(max_len, refs.back().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t target_tabs = max_len / tab_width + 1;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < images->size(); ++i) {
|
||||||
|
size_t tabs_used = refs[i].size() / tab_width;
|
||||||
|
size_t tabs_needed = target_tabs > tabs_used ? target_tabs - tabs_used : 1;
|
||||||
|
fmt::print("{}{}{}\n", refs[i], std::string(tabs_needed, '\t'),
|
||||||
|
(*images)[i].path.filename().string());
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prints every OciImageConfig field for a human to read, without mounting or
|
||||||
|
// running the image. Extend this whenever OciImageConfig gains a new field.
|
||||||
|
int inspect_image_command(const std::filesystem::path& image_tar) {
|
||||||
|
auto config = read_oci_image_config(image_tar);
|
||||||
|
if (!config) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt::print("Image: {}\n", image_tar.string());
|
||||||
|
|
||||||
|
if (config->user.empty()) {
|
||||||
|
fmt::print("User: (not set)\n");
|
||||||
|
} else if (config->group.empty()) {
|
||||||
|
fmt::print("User: {}\n", config->user);
|
||||||
|
} else {
|
||||||
|
fmt::print("User: {}:{}\n", config->user, config->group);
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt::print("Command: {}\n",
|
||||||
|
config->command.empty() ? "(not set)" : fmt::to_string(fmt::join(config->command, " ")));
|
||||||
|
|
||||||
|
if (config->exposed_ports.empty()) {
|
||||||
|
fmt::print("Exposed ports: (none)\n");
|
||||||
|
} else {
|
||||||
|
fmt::print("Exposed ports:\n");
|
||||||
|
for (const auto& port : config->exposed_ports) {
|
||||||
|
fmt::print(" {}/{}\n", port.port, port.protocol == OciPortProtocol::tcp ? "tcp" : "udp");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config->env.empty()) {
|
||||||
|
fmt::print("Env: (none)\n");
|
||||||
|
} else {
|
||||||
|
fmt::print("Env:\n");
|
||||||
|
for (const auto& e : config->env) {
|
||||||
|
fmt::print(" {}\n", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config->volumes.empty()) {
|
||||||
|
fmt::print("Volumes: (none)\n");
|
||||||
|
} else {
|
||||||
|
fmt::print("Volumes:\n");
|
||||||
|
for (const auto& v : config->volumes) {
|
||||||
|
fmt::print(" {}\n", v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int create_volume_command(const std::string& name, const std::string& directory,
|
||||||
|
const std::filesystem::path& config_path, AppConfig& config) {
|
||||||
|
if (!is_valid_volume_name(name)) {
|
||||||
|
spdlog::error("volume name '{}' must not contain '/'", name);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 list_volumes_command(const AppConfig& config) {
|
||||||
|
size_t max_len = 0;
|
||||||
|
for (const auto& volume : config.volumes) {
|
||||||
|
max_len = std::max(max_len, volume.name.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t target_tabs = max_len / tab_width + 1;
|
||||||
|
|
||||||
|
for (const auto& volume : config.volumes) {
|
||||||
|
size_t tabs_used = volume.name.size() / tab_width;
|
||||||
|
size_t tabs_needed = target_tabs > tabs_used ? target_tabs - tabs_used : 1;
|
||||||
|
fmt::print("{}{}{}\n", volume.name, std::string(tabs_needed, '\t'), volume.directory);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int list_processes_command() {
|
||||||
|
auto sessions = list_sessions();
|
||||||
|
|
||||||
|
std::vector<std::string> pids;
|
||||||
|
std::vector<std::string> names;
|
||||||
|
pids.reserve(sessions.size());
|
||||||
|
names.reserve(sessions.size());
|
||||||
|
size_t max_pid_len = 0;
|
||||||
|
size_t max_name_len = 0;
|
||||||
|
for (const auto& session : sessions) {
|
||||||
|
pids.push_back(fmt::format("{}", session.pid));
|
||||||
|
names.push_back(session.container_name);
|
||||||
|
max_pid_len = std::max(max_pid_len, pids.back().size());
|
||||||
|
max_name_len = std::max(max_name_len, names.back().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Applied independently to each of the two variable-width columns.
|
||||||
|
size_t pid_target_tabs = max_pid_len / tab_width + 1;
|
||||||
|
size_t name_target_tabs = max_name_len / tab_width + 1;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < sessions.size(); ++i) {
|
||||||
|
size_t pid_tabs_used = pids[i].size() / tab_width;
|
||||||
|
size_t pid_tabs_needed = pid_target_tabs > pid_tabs_used ? pid_target_tabs - pid_tabs_used : 1;
|
||||||
|
size_t name_tabs_used = names[i].size() / tab_width;
|
||||||
|
size_t name_tabs_needed = name_target_tabs > name_tabs_used ? name_target_tabs - name_tabs_used : 1;
|
||||||
|
fmt::print("{}{}{}{}{}\n", pids[i], std::string(pid_tabs_needed, '\t'), names[i],
|
||||||
|
std::string(name_tabs_needed, '\t'), sessions[i].running ? "running" : "exited");
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int clean_processes_command() {
|
||||||
|
for (const auto& session : clean_stale_sessions()) {
|
||||||
|
fmt::print("removed stale pid file for '{}' (pid {})\n", session.container_name, session.pid);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int delete_volume_command(const std::string& name, const std::filesystem::path& config_path,
|
||||||
|
AppConfig& config, bool delete_directory) {
|
||||||
|
auto it = std::find_if(config.volumes.begin(), config.volumes.end(),
|
||||||
|
[&](const VolumeEntry& volume) { return volume.name == name; });
|
||||||
|
if (it == config.volumes.end()) {
|
||||||
|
spdlog::error("no volume named '{}' exists", name);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delete_directory) {
|
||||||
|
if (!std::filesystem::exists(it->directory)) {
|
||||||
|
spdlog::warn("directory {} does not exist, nothing to delete", it->directory);
|
||||||
|
}
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::remove_all(it->directory, ec);
|
||||||
|
if (ec) {
|
||||||
|
spdlog::error("failed to delete directory {}: {}", it->directory, ec.message());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.volumes.erase(it);
|
||||||
|
if (!write_config_file(config_path, config)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt::print("deleted volume '{}'\n", name);
|
||||||
|
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,
|
||||||
|
const std::optional<std::string>& hostname,
|
||||||
|
const std::vector<std::pair<std::string, std::string>>& volume_specs,
|
||||||
|
const std::vector<EnvSpec>& env_specs, bool daemonize_flag, const AppConfig& app_config) {
|
||||||
|
// Only depends on image_tar, so this can run before mount_image() -- moved
|
||||||
|
// up here (rather than right before the run_bwrap() call, as before) so
|
||||||
|
// daemonize() below can use the real container name for the log file from
|
||||||
|
// its very first line, not just after a later rename.
|
||||||
|
auto image_ref = read_image_ref(image_tar);
|
||||||
|
std::string container_name =
|
||||||
|
image_ref ? fmt::format("{}:{}", image_ref->name, image_ref->tag) : image_tar.stem().string();
|
||||||
|
|
||||||
|
if (daemonize_flag) {
|
||||||
|
auto result = daemonize(container_name);
|
||||||
|
if (result) {
|
||||||
|
// Parent (or a hard daemonize failure before ever forking) -- report
|
||||||
|
// and return immediately. The child falls through below instead.
|
||||||
|
if (result->pid) {
|
||||||
|
fmt::print("started in background: pid {}, log: {}\n", *result->pid, result->log_path);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
spdlog::error("failed to start in background{}", result->log_path.empty()
|
||||||
|
? ""
|
||||||
|
: fmt::format(" (see log: {})", result->log_path));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto mounted = mount_image(image_tar);
|
||||||
|
if (!mounted) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
fmt::print("mounted image at: {} (layer {})\n", mounted->merged_path, mounted->top_layer_id);
|
||||||
|
|
||||||
|
auto config = read_oci_image_config(image_tar);
|
||||||
|
|
||||||
|
bool ok = true;
|
||||||
|
|
||||||
|
std::vector<ResolvedVolumeMount> volume_mounts;
|
||||||
|
for (const auto& [spec, container_path] : volume_specs) {
|
||||||
|
if (container_path.empty() || container_path.front() != '/') {
|
||||||
|
spdlog::error("volume container path '{}' must be absolute", container_path);
|
||||||
|
ok = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bool duplicate = std::any_of(
|
||||||
|
volume_mounts.begin(), volume_mounts.end(),
|
||||||
|
[&](const ResolvedVolumeMount& mount) { return mount.container_path == container_path; });
|
||||||
|
if (duplicate) {
|
||||||
|
spdlog::error("volume container path '{}' is mounted more than once", container_path);
|
||||||
|
ok = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// use_nsenter also governs whether resolve_volume_mount() needs nsenter to see
|
||||||
|
// the image's own content when populating an empty volume -- the same
|
||||||
|
// rootless-mount visibility constraint run_bwrap() itself works around.
|
||||||
|
auto resolved =
|
||||||
|
resolve_volume_mount(spec, container_path, app_config, mounted->merged_path, use_nsenter);
|
||||||
|
if (!resolved) {
|
||||||
|
ok = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
volume_mounts.push_back(std::move(*resolved));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto resolved_env = resolve_env_specs(env_specs);
|
||||||
|
if (!resolved_env) {
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Falls back to the image's own declared user (config.User) when --user wasn't
|
||||||
|
// given on the command line, rather than always defaulting to root.
|
||||||
|
std::optional<std::string> effective_user = user;
|
||||||
|
std::optional<std::string> effective_group = group;
|
||||||
|
if (!effective_user && config && !config->user.empty()) {
|
||||||
|
effective_user = config->user;
|
||||||
|
effective_group = config->group.empty() ? std::nullopt : std::optional<std::string>(config->group);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<ResolvedUser> resolved_user;
|
||||||
|
if (effective_user) {
|
||||||
|
resolved_user = resolve_user_and_group(*effective_user, effective_group, mounted->merged_path);
|
||||||
|
if (!resolved_user) {
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> command = requested_command;
|
||||||
|
if (command.empty()) {
|
||||||
|
command = (config && !config->command.empty()) ? config->command
|
||||||
|
: std::vector<std::string>{"/bin/sh"};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::function<void(pid_t)> on_bwrap_pid_known;
|
||||||
|
if (daemonize_flag) {
|
||||||
|
on_bwrap_pid_known = [&](pid_t pid) { report_daemon_started(container_name, pid); };
|
||||||
|
}
|
||||||
|
|
||||||
|
int exit_code = -1;
|
||||||
|
if (ok) {
|
||||||
|
exit_code = run_bwrap(mounted->merged_path, command, use_nsenter, volume_mounts, resolved_user, hostname,
|
||||||
|
container_name, *resolved_env, on_bwrap_pid_known);
|
||||||
|
if (exit_code < 0) {
|
||||||
|
spdlog::error("failed to run bwrap");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!unmount_layer(mounted->top_layer_id)) {
|
||||||
|
spdlog::error("failed to unmount layer {}", mounted->top_layer_id);
|
||||||
|
}
|
||||||
|
if (!cleanup_layer_chain(mounted->top_layer_id)) {
|
||||||
|
spdlog::error("failed to clean up layer {}", mounted->top_layer_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (!ok || exit_code < 0) ? 1 : exit_code;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int dispatch_command(const ParsedArgs& args, const std::filesystem::path& config_path, AppConfig& config) {
|
||||||
|
// Exhaustive over every Mode enumerator, deliberately with no default:
|
||||||
|
// -Wswitch (this project builds at warning_level=3) then forces a
|
||||||
|
// warning/error if a future Mode value is ever added without a matching
|
||||||
|
// case here, instead of silently falling through to the wrong command.
|
||||||
|
switch (args.mode) {
|
||||||
|
case Mode::none:
|
||||||
|
// Unreachable: parse_args() already rejects Mode::none before
|
||||||
|
// dispatch_command() is ever called.
|
||||||
|
spdlog::error("internal error: no mode selected");
|
||||||
|
return 1;
|
||||||
|
case Mode::mount:
|
||||||
|
return mount_command(args.mode_arg);
|
||||||
|
case Mode::unmount:
|
||||||
|
return unmount_image(args.mode_arg);
|
||||||
|
case Mode::cleanup:
|
||||||
|
return cleanup_image(args.mode_arg);
|
||||||
|
case Mode::list_images:
|
||||||
|
return list_images_command(args.mode_arg);
|
||||||
|
case Mode::inspect:
|
||||||
|
return inspect_image_command(args.mode_arg);
|
||||||
|
case Mode::volume:
|
||||||
|
return create_volume_command(args.volume_specs.front().first, args.volume_specs.front().second,
|
||||||
|
config_path, config);
|
||||||
|
case Mode::list_volumes:
|
||||||
|
return list_volumes_command(config);
|
||||||
|
case Mode::delete_volume:
|
||||||
|
return delete_volume_command(args.mode_arg, config_path, config, false);
|
||||||
|
case Mode::delete_volume_full:
|
||||||
|
return delete_volume_command(args.mode_arg, config_path, config, true);
|
||||||
|
case Mode::list_processes:
|
||||||
|
return list_processes_command();
|
||||||
|
case Mode::clean_processes:
|
||||||
|
return clean_processes_command();
|
||||||
|
case Mode::test:
|
||||||
|
return run_self_tests();
|
||||||
|
case Mode::exec:
|
||||||
|
return exec_in_session(*args.exec_pid, args.command);
|
||||||
|
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
|
||||||
|
// visible; nsenter into it then fails ("reassociate to namespace 'ns/user'
|
||||||
|
// failed: Invalid argument") since we're already in that same namespace.
|
||||||
|
bool use_nsenter = !args.disable_nsenter && geteuid() != 0;
|
||||||
|
if (geteuid() == 0 && !args.disable_nsenter) {
|
||||||
|
spdlog::debug("running as root; skipping nsenter (the mount is already directly visible)");
|
||||||
|
}
|
||||||
|
return run_container(args.mode_arg, args.command, use_nsenter, args.user_flag, args.group_flag,
|
||||||
|
args.hostname_flag, args.volume_specs, args.env_specs, args.daemonize_flag,
|
||||||
|
config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1; // unreachable
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// Copyright (C) 2026 Viorel Munteanu
|
||||||
|
//
|
||||||
|
// This program is free software; you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation; either version 2 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License along
|
||||||
|
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
#include "cli_args.h"
|
||||||
|
#include "config_file.h"
|
||||||
|
|
||||||
|
// Runs whichever command args.mode selects (an exhaustive switch over Mode --
|
||||||
|
// see commands.cpp -- so a Mode value ever added without a matching case
|
||||||
|
// triggers a -Wswitch warning/error rather than silently falling through).
|
||||||
|
int dispatch_command(const ParsedArgs& args, const std::filesystem::path& config_path, AppConfig& config);
|
||||||
+6
-874
@@ -14,655 +14,14 @@
|
|||||||
// with this program; if not, write to the Free Software Foundation, Inc.,
|
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <array>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <functional>
|
|
||||||
#include <getopt.h>
|
|
||||||
#include <optional>
|
|
||||||
#include <string>
|
|
||||||
#include <string_view>
|
|
||||||
#include <unistd.h>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <fmt/core.h>
|
|
||||||
#include <fmt/ranges.h>
|
|
||||||
#include <spdlog/cfg/env.h>
|
#include <spdlog/cfg/env.h>
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
#include "bwrap.h"
|
#include "cli_args.h"
|
||||||
#include "config.h"
|
#include "commands.h"
|
||||||
#include "config_file.h"
|
#include "config_file.h"
|
||||||
#include "containers_storage.h"
|
|
||||||
#include "daemonize.h"
|
|
||||||
#include "env_spec.h"
|
|
||||||
#include "exec_session.h"
|
|
||||||
#include "oci_image.h"
|
|
||||||
#include "pid_file.h"
|
|
||||||
#include "process.h"
|
|
||||||
#include "user_spec.h"
|
|
||||||
#include "volume_mount.h"
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
constexpr std::array<std::string_view, 2> required_tools = {"containers-storage", "bwrap"};
|
|
||||||
|
|
||||||
// Shared by list_images_command()/list_volumes_command()/list_processes_command():
|
|
||||||
// pad each entry with tabs (not spaces) so columns line up on an 8-column tab
|
|
||||||
// stop past the longest entry in that column, however long that is.
|
|
||||||
constexpr size_t tab_width = 8;
|
|
||||||
|
|
||||||
enum class Mode {
|
|
||||||
none,
|
|
||||||
mount,
|
|
||||||
unmount,
|
|
||||||
test,
|
|
||||||
run,
|
|
||||||
cleanup,
|
|
||||||
list_images,
|
|
||||||
volume,
|
|
||||||
list_volumes,
|
|
||||||
delete_volume,
|
|
||||||
delete_volume_full,
|
|
||||||
inspect,
|
|
||||||
list_processes,
|
|
||||||
clean_processes,
|
|
||||||
exec
|
|
||||||
};
|
|
||||||
|
|
||||||
// --log-level/--user/--group/--list-volumes/--delete-volume[-full]/--hostname/
|
|
||||||
// --list-processes/--clean-processes/--env/--env-file have no short form
|
|
||||||
// (--log-level's was freed up so -l could become --list-images; -u is already
|
|
||||||
// --umount; the rest have no natural free letter left, or don't need one), so
|
|
||||||
// they need long-option vals outside the printable-char range short options use.
|
|
||||||
namespace options {
|
|
||||||
constexpr int log_level = 256;
|
|
||||||
constexpr int user = 257;
|
|
||||||
constexpr int group = 258;
|
|
||||||
constexpr int list_volumes = 259;
|
|
||||||
constexpr int delete_volume = 260;
|
|
||||||
constexpr int delete_volume_full = 261;
|
|
||||||
constexpr int hostname = 262;
|
|
||||||
constexpr int list_processes = 263;
|
|
||||||
constexpr int clean_processes = 264;
|
|
||||||
constexpr int env = 265;
|
|
||||||
constexpr int env_file = 266;
|
|
||||||
} // namespace options
|
|
||||||
|
|
||||||
constexpr std::array<struct option, 25> long_options = {{
|
|
||||||
{"help", no_argument, nullptr, 'h'},
|
|
||||||
{"version", no_argument, nullptr, 'V'},
|
|
||||||
{"test", no_argument, nullptr, 't'},
|
|
||||||
{"log-level", required_argument, nullptr, options::log_level},
|
|
||||||
{"mount", required_argument, nullptr, 'm'},
|
|
||||||
{"umount", required_argument, nullptr, 'u'},
|
|
||||||
{"run", required_argument, nullptr, 'r'},
|
|
||||||
{"cleanup", required_argument, nullptr, 'c'},
|
|
||||||
{"no-nsenter", no_argument, nullptr, 'n'},
|
|
||||||
{"daemonize", no_argument, nullptr, 'D'},
|
|
||||||
{"list-images", required_argument, nullptr, 'l'},
|
|
||||||
{"user", required_argument, nullptr, options::user},
|
|
||||||
{"group", required_argument, nullptr, options::group},
|
|
||||||
{"volume", required_argument, nullptr, 'v'},
|
|
||||||
{"list-volumes", no_argument, nullptr, options::list_volumes},
|
|
||||||
{"delete-volume", required_argument, nullptr, options::delete_volume},
|
|
||||||
{"delete-volume-full", required_argument, nullptr, options::delete_volume_full},
|
|
||||||
{"inspect", required_argument, nullptr, 'i'},
|
|
||||||
{"exec", required_argument, nullptr, 'e'},
|
|
||||||
{"hostname", required_argument, nullptr, options::hostname},
|
|
||||||
{"list-processes", no_argument, nullptr, options::list_processes},
|
|
||||||
{"clean-processes", no_argument, nullptr, options::clean_processes},
|
|
||||||
{"env", required_argument, nullptr, options::env},
|
|
||||||
{"env-file", required_argument, nullptr, options::env_file},
|
|
||||||
{nullptr, 0, nullptr, 0},
|
|
||||||
}};
|
|
||||||
|
|
||||||
void print_usage(const char* prog) {
|
|
||||||
fmt::print(
|
|
||||||
"usage: {0} -m|--mount <image.tar>\n"
|
|
||||||
" {0} -r|--run <image.tar> [-v <name-or-dir> <container-path>]... [-- <command> [args...]]\n"
|
|
||||||
" {0} -u|--umount <layer-id>\n"
|
|
||||||
" {0} -c|--cleanup <layer-id>\n"
|
|
||||||
" {0} -l|--list-images <directory>\n"
|
|
||||||
" {0} -i|--inspect <image.tar>\n"
|
|
||||||
" {0} -e|--exec <pid> [-- <command> [args...]]\n"
|
|
||||||
" {0} -v|--volume <name> <directory>\n"
|
|
||||||
" {0} --list-volumes\n"
|
|
||||||
" {0} --delete-volume <name>\n"
|
|
||||||
" {0} --delete-volume-full <name>\n"
|
|
||||||
" {0} --list-processes\n"
|
|
||||||
" {0} --clean-processes\n"
|
|
||||||
" {0} -t|--test\n"
|
|
||||||
" {0} -h|--help\n"
|
|
||||||
" {0} -V|--version\n"
|
|
||||||
"\n"
|
|
||||||
"options:\n"
|
|
||||||
" -m, --mount <image.tar> validate and mount an OCI Image Layout tar\n"
|
|
||||||
" -r, --run <image.tar> mount, run bwrap in the foreground (default\n"
|
|
||||||
" command: the image's own Entrypoint/Cmd if set,\n"
|
|
||||||
" else /bin/sh; pass -- <command> [args...] to\n"
|
|
||||||
" override), then unmount and clean up when it\n"
|
|
||||||
" exits\n"
|
|
||||||
" -u, --umount <layer-id> unmount a previously mounted image layer (the\n"
|
|
||||||
" ID printed by --mount/--run, or from\n"
|
|
||||||
" `containers-storage layers`)\n"
|
|
||||||
" -c, --cleanup <layer-id> delete a layer and its ancestor chain from\n"
|
|
||||||
" local storage (unmount it first with --umount)\n"
|
|
||||||
" -n, --no-nsenter with --run, bind the mount directly instead of\n"
|
|
||||||
" nsenter-ing into fuse-overlayfs's namespace\n"
|
|
||||||
" (this is automatic when running as root, where\n"
|
|
||||||
" the mount is already directly visible; pass\n"
|
|
||||||
" this to force it off otherwise)\n"
|
|
||||||
" -D, --daemonize with --run, fork into the background: detaches\n"
|
|
||||||
" from the controlling terminal (setsid()) and\n"
|
|
||||||
" ignores SIGHUP, redirecting stdin from /dev/null\n"
|
|
||||||
" and stdout/stderr to a log file under\n"
|
|
||||||
" $XDG_STATE_HOME/slocker-lite/logs/. Prints the\n"
|
|
||||||
" session's pid and log path, then returns\n"
|
|
||||||
" immediately -- the same pid --list-processes/\n"
|
|
||||||
" -e/--exec use\n"
|
|
||||||
" --user <user> with --run, run the command as this user (name\n"
|
|
||||||
" or numeric uid) instead of the image's own\n"
|
|
||||||
" declared user (or root, if it declares none),\n"
|
|
||||||
" resolved against the image's own /etc/passwd;\n"
|
|
||||||
" only takes effect when --run executes as root\n"
|
|
||||||
" (no user namespace involved)\n"
|
|
||||||
" --group <group> with --user, use this group (name or numeric\n"
|
|
||||||
" gid) instead of the user's own primary group\n"
|
|
||||||
" --hostname <name> with --run, set the sandbox's hostname (only\n"
|
|
||||||
" takes effect if the running kernel supports\n"
|
|
||||||
" --unshare-uts; otherwise ignored with a\n"
|
|
||||||
" warning)\n"
|
|
||||||
" --env VAR=VALUE with --run, set an environment variable in the\n"
|
|
||||||
" sandbox (overrides the default PATH/HOME/PWD/\n"
|
|
||||||
" TERM if given the same name). May be repeated;\n"
|
|
||||||
" combined with --env-file in command-line order,\n"
|
|
||||||
" each later one winning over an earlier one for\n"
|
|
||||||
" the same name\n"
|
|
||||||
" --env-file <file> with --run, load environment variables from\n"
|
|
||||||
" <file> (one VAR=VALUE per line; blank lines and\n"
|
|
||||||
" #-comments are skipped). May be repeated\n"
|
|
||||||
" -l, --list-images <dir> list OCI Image Layout tars (*.tar, *.tar.*) found\n"
|
|
||||||
" directly in <dir>, with their name:tag\n"
|
|
||||||
" -i, --inspect <image.tar> print an image's declared user, exposed ports,\n"
|
|
||||||
" env, volumes, and default command, without\n"
|
|
||||||
" mounting or running it\n"
|
|
||||||
" -e, --exec <pid> join a running --run session (pid must be one\n"
|
|
||||||
" shown by --list-processes as \"running\") and run\n"
|
|
||||||
" a command inside its container; pass\n"
|
|
||||||
" -- <command> [args...] to specify it\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. With --run, instead\n"
|
|
||||||
" mount a volume into the sandbox: <name> is a\n"
|
|
||||||
" named volume or, if it contains '/', a host\n"
|
|
||||||
" directory (created if missing); <dir> is the\n"
|
|
||||||
" absolute path inside the container to mount it\n"
|
|
||||||
" at. May be repeated with --run. If the host\n"
|
|
||||||
" directory is empty and the image already has\n"
|
|
||||||
" content there, it's copied in first\n"
|
|
||||||
" --list-volumes list all named volumes (see -v/--volume) with\n"
|
|
||||||
" their host directory\n"
|
|
||||||
" --delete-volume <name>\n"
|
|
||||||
" remove a named volume from the config (the host\n"
|
|
||||||
" directory is left untouched)\n"
|
|
||||||
" --delete-volume-full <name>\n"
|
|
||||||
" like --delete-volume, but also recursively\n"
|
|
||||||
" deletes the volume's host directory\n"
|
|
||||||
" --list-processes list running --run sessions found by their pid\n"
|
|
||||||
" files under $XDG_STATE_HOME/slocker-lite/run/,\n"
|
|
||||||
" with their pid, container name, and status\n"
|
|
||||||
" (running or exited)\n"
|
|
||||||
" --clean-processes remove stale pid files (see --list-processes)\n"
|
|
||||||
" left behind by sessions that are no longer\n"
|
|
||||||
" running\n"
|
|
||||||
" -t, --test run the test suite\n"
|
|
||||||
" --log-level <level> set log verbosity (trace, debug, info, warn,\n"
|
|
||||||
" error, critical, off)\n"
|
|
||||||
" -h, --help print this help and exit\n"
|
|
||||||
" -V, --version print version information and exit\n",
|
|
||||||
prog);
|
|
||||||
}
|
|
||||||
|
|
||||||
void print_version() {
|
|
||||||
fmt::print(
|
|
||||||
"{} {}\n"
|
|
||||||
"Licensed under GNU GPL version 2 or later <https://gnu.org/licenses/gpl-2.0.html>\n"
|
|
||||||
"This is free software: you are free to change and redistribute it.\n"
|
|
||||||
"There is NO WARRANTY, to the extent permitted by law.\n",
|
|
||||||
PACKAGE, VERSION);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool apply_log_level(std::string_view name) {
|
|
||||||
constexpr std::array<std::string_view, 7> valid_levels = {
|
|
||||||
"trace", "debug", "info", "warn", "error", "critical", "off"};
|
|
||||||
for (auto level : valid_levels) {
|
|
||||||
if (level == name) {
|
|
||||||
spdlog::set_level(spdlog::level::from_str(std::string(name)));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
spdlog::error("invalid log level: {}", name);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int run_tests() {
|
|
||||||
for (const auto& arg : detect_bwrap_unshare_args()) {
|
|
||||||
fmt::print("{}\n", arg);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
bool check_required_dependencies() {
|
|
||||||
bool all_found = true;
|
|
||||||
for (auto name : required_tools) {
|
|
||||||
if (!find_in_path(name)) {
|
|
||||||
spdlog::error("required dependency not found in PATH: {}", name);
|
|
||||||
all_found = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return all_found;
|
|
||||||
}
|
|
||||||
|
|
||||||
int unmount_image(const std::string& layer_id) {
|
|
||||||
if (!check_required_dependencies()) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (!unmount_layer(layer_id)) {
|
|
||||||
spdlog::error("failed to unmount layer {}", layer_id);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
fmt::print("unmounted layer {}\n", layer_id);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MountedImage {
|
|
||||||
std::string merged_path;
|
|
||||||
std::string top_layer_id;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::optional<MountedImage> mount_image(const std::filesystem::path& image_tar) {
|
|
||||||
if (!check_required_dependencies()) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto mount_program = find_in_path("fuse-overlayfs");
|
|
||||||
if (!mount_program) {
|
|
||||||
spdlog::error("fuse-overlayfs not found in PATH");
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
g_mount_program = mount_program->string();
|
|
||||||
|
|
||||||
auto layers = read_oci_layers(image_tar);
|
|
||||||
if (!layers) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string parent_id;
|
|
||||||
for (const auto& layer : *layers) {
|
|
||||||
std::filesystem::path tmp_file =
|
|
||||||
std::filesystem::temp_directory_path() /
|
|
||||||
fmt::format("slocker-lite-{}-{}", getpid(), oci_digest_hex(layer.digest));
|
|
||||||
|
|
||||||
if (!extract_blob_to_file(image_tar, oci_digest_hex(layer.digest), tmp_file)) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto layer_id = import_layer(tmp_file, parent_id);
|
|
||||||
std::filesystem::remove(tmp_file);
|
|
||||||
if (!layer_id) {
|
|
||||||
spdlog::error("failed to import layer {}", layer.digest);
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
parent_id = *layer_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto merged = mount_layer(parent_id);
|
|
||||||
if (!merged) {
|
|
||||||
spdlog::error("failed to mount assembled image");
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
return MountedImage{*merged, parent_id};
|
|
||||||
}
|
|
||||||
|
|
||||||
int cleanup_image(const std::string& layer_id) {
|
|
||||||
if (!check_required_dependencies()) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (!cleanup_layer_chain(layer_id)) {
|
|
||||||
spdlog::error("failed to clean up layer {}", layer_id);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
fmt::print("cleaned up layer {}\n", layer_id);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int list_images_command(const std::filesystem::path& dir) {
|
|
||||||
auto images = list_oci_images(dir);
|
|
||||||
if (!images) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::string> refs;
|
|
||||||
refs.reserve(images->size());
|
|
||||||
size_t max_len = 0;
|
|
||||||
for (const auto& ref : *images) {
|
|
||||||
refs.push_back(fmt::format("{}:{}", ref.name, ref.tag));
|
|
||||||
max_len = std::max(max_len, refs.back().size());
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t target_tabs = max_len / tab_width + 1;
|
|
||||||
|
|
||||||
for (size_t i = 0; i < images->size(); ++i) {
|
|
||||||
size_t tabs_used = refs[i].size() / tab_width;
|
|
||||||
size_t tabs_needed = target_tabs > tabs_used ? target_tabs - tabs_used : 1;
|
|
||||||
fmt::print("{}{}{}\n", refs[i], std::string(tabs_needed, '\t'),
|
|
||||||
(*images)[i].path.filename().string());
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prints every OciImageConfig field for a human to read, without mounting or
|
|
||||||
// running the image. Extend this whenever OciImageConfig gains a new field.
|
|
||||||
int inspect_image_command(const std::filesystem::path& image_tar) {
|
|
||||||
auto config = read_oci_image_config(image_tar);
|
|
||||||
if (!config) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt::print("Image: {}\n", image_tar.string());
|
|
||||||
|
|
||||||
if (config->user.empty()) {
|
|
||||||
fmt::print("User: (not set)\n");
|
|
||||||
} else if (config->group.empty()) {
|
|
||||||
fmt::print("User: {}\n", config->user);
|
|
||||||
} else {
|
|
||||||
fmt::print("User: {}:{}\n", config->user, config->group);
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt::print("Command: {}\n",
|
|
||||||
config->command.empty() ? "(not set)" : fmt::to_string(fmt::join(config->command, " ")));
|
|
||||||
|
|
||||||
if (config->exposed_ports.empty()) {
|
|
||||||
fmt::print("Exposed ports: (none)\n");
|
|
||||||
} else {
|
|
||||||
fmt::print("Exposed ports:\n");
|
|
||||||
for (const auto& port : config->exposed_ports) {
|
|
||||||
fmt::print(" {}/{}\n", port.port, port.protocol == OciPortProtocol::tcp ? "tcp" : "udp");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config->env.empty()) {
|
|
||||||
fmt::print("Env: (none)\n");
|
|
||||||
} else {
|
|
||||||
fmt::print("Env:\n");
|
|
||||||
for (const auto& e : config->env) {
|
|
||||||
fmt::print(" {}\n", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config->volumes.empty()) {
|
|
||||||
fmt::print("Volumes: (none)\n");
|
|
||||||
} else {
|
|
||||||
fmt::print("Volumes:\n");
|
|
||||||
for (const auto& v : config->volumes) {
|
|
||||||
fmt::print(" {}\n", v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int create_volume_command(const std::string& name, const std::string& directory,
|
|
||||||
const std::filesystem::path& config_path, AppConfig& config) {
|
|
||||||
if (!is_valid_volume_name(name)) {
|
|
||||||
spdlog::error("volume name '{}' must not contain '/'", name);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 list_volumes_command(const AppConfig& config) {
|
|
||||||
size_t max_len = 0;
|
|
||||||
for (const auto& volume : config.volumes) {
|
|
||||||
max_len = std::max(max_len, volume.name.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t target_tabs = max_len / tab_width + 1;
|
|
||||||
|
|
||||||
for (const auto& volume : config.volumes) {
|
|
||||||
size_t tabs_used = volume.name.size() / tab_width;
|
|
||||||
size_t tabs_needed = target_tabs > tabs_used ? target_tabs - tabs_used : 1;
|
|
||||||
fmt::print("{}{}{}\n", volume.name, std::string(tabs_needed, '\t'), volume.directory);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int list_processes_command() {
|
|
||||||
auto sessions = list_sessions();
|
|
||||||
|
|
||||||
std::vector<std::string> pids;
|
|
||||||
std::vector<std::string> names;
|
|
||||||
pids.reserve(sessions.size());
|
|
||||||
names.reserve(sessions.size());
|
|
||||||
size_t max_pid_len = 0;
|
|
||||||
size_t max_name_len = 0;
|
|
||||||
for (const auto& session : sessions) {
|
|
||||||
pids.push_back(fmt::format("{}", session.pid));
|
|
||||||
names.push_back(session.container_name);
|
|
||||||
max_pid_len = std::max(max_pid_len, pids.back().size());
|
|
||||||
max_name_len = std::max(max_name_len, names.back().size());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Applied independently to each of the two variable-width columns.
|
|
||||||
size_t pid_target_tabs = max_pid_len / tab_width + 1;
|
|
||||||
size_t name_target_tabs = max_name_len / tab_width + 1;
|
|
||||||
|
|
||||||
for (size_t i = 0; i < sessions.size(); ++i) {
|
|
||||||
size_t pid_tabs_used = pids[i].size() / tab_width;
|
|
||||||
size_t pid_tabs_needed = pid_target_tabs > pid_tabs_used ? pid_target_tabs - pid_tabs_used : 1;
|
|
||||||
size_t name_tabs_used = names[i].size() / tab_width;
|
|
||||||
size_t name_tabs_needed = name_target_tabs > name_tabs_used ? name_target_tabs - name_tabs_used : 1;
|
|
||||||
fmt::print("{}{}{}{}{}\n", pids[i], std::string(pid_tabs_needed, '\t'), names[i],
|
|
||||||
std::string(name_tabs_needed, '\t'), sessions[i].running ? "running" : "exited");
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int clean_processes_command() {
|
|
||||||
for (const auto& session : clean_stale_sessions()) {
|
|
||||||
fmt::print("removed stale pid file for '{}' (pid {})\n", session.container_name, session.pid);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int delete_volume_command(const std::string& name, const std::filesystem::path& config_path,
|
|
||||||
AppConfig& config, bool delete_directory) {
|
|
||||||
auto it = std::find_if(config.volumes.begin(), config.volumes.end(),
|
|
||||||
[&](const VolumeEntry& volume) { return volume.name == name; });
|
|
||||||
if (it == config.volumes.end()) {
|
|
||||||
spdlog::error("no volume named '{}' exists", name);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (delete_directory) {
|
|
||||||
if (!std::filesystem::exists(it->directory)) {
|
|
||||||
spdlog::warn("directory {} does not exist, nothing to delete", it->directory);
|
|
||||||
}
|
|
||||||
std::error_code ec;
|
|
||||||
std::filesystem::remove_all(it->directory, ec);
|
|
||||||
if (ec) {
|
|
||||||
spdlog::error("failed to delete directory {}: {}", it->directory, ec.message());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.volumes.erase(it);
|
|
||||||
if (!write_config_file(config_path, config)) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt::print("deleted volume '{}'\n", name);
|
|
||||||
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,
|
|
||||||
const std::optional<std::string>& hostname,
|
|
||||||
const std::vector<std::pair<std::string, std::string>>& volume_specs,
|
|
||||||
const std::vector<EnvSpec>& env_specs, bool daemonize_flag, const AppConfig& app_config) {
|
|
||||||
// Only depends on image_tar, so this can run before mount_image() -- moved
|
|
||||||
// up here (rather than right before the run_bwrap() call, as before) so
|
|
||||||
// daemonize() below can use the real container name for the log file from
|
|
||||||
// its very first line, not just after a later rename.
|
|
||||||
auto image_ref = read_image_ref(image_tar);
|
|
||||||
std::string container_name =
|
|
||||||
image_ref ? fmt::format("{}:{}", image_ref->name, image_ref->tag) : image_tar.stem().string();
|
|
||||||
|
|
||||||
if (daemonize_flag) {
|
|
||||||
auto result = daemonize(container_name);
|
|
||||||
if (result) {
|
|
||||||
// Parent (or a hard daemonize failure before ever forking) -- report
|
|
||||||
// and return immediately. The child falls through below instead.
|
|
||||||
if (result->pid) {
|
|
||||||
fmt::print("started in background: pid {}, log: {}\n", *result->pid, result->log_path);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
spdlog::error("failed to start in background{}", result->log_path.empty()
|
|
||||||
? ""
|
|
||||||
: fmt::format(" (see log: {})", result->log_path));
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto mounted = mount_image(image_tar);
|
|
||||||
if (!mounted) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
fmt::print("mounted image at: {} (layer {})\n", mounted->merged_path, mounted->top_layer_id);
|
|
||||||
|
|
||||||
auto config = read_oci_image_config(image_tar);
|
|
||||||
|
|
||||||
bool ok = true;
|
|
||||||
|
|
||||||
std::vector<ResolvedVolumeMount> volume_mounts;
|
|
||||||
for (const auto& [spec, container_path] : volume_specs) {
|
|
||||||
if (container_path.empty() || container_path.front() != '/') {
|
|
||||||
spdlog::error("volume container path '{}' must be absolute", container_path);
|
|
||||||
ok = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
bool duplicate = std::any_of(
|
|
||||||
volume_mounts.begin(), volume_mounts.end(),
|
|
||||||
[&](const ResolvedVolumeMount& mount) { return mount.container_path == container_path; });
|
|
||||||
if (duplicate) {
|
|
||||||
spdlog::error("volume container path '{}' is mounted more than once", container_path);
|
|
||||||
ok = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// use_nsenter also governs whether resolve_volume_mount() needs nsenter to see
|
|
||||||
// the image's own content when populating an empty volume -- the same
|
|
||||||
// rootless-mount visibility constraint run_bwrap() itself works around.
|
|
||||||
auto resolved =
|
|
||||||
resolve_volume_mount(spec, container_path, app_config, mounted->merged_path, use_nsenter);
|
|
||||||
if (!resolved) {
|
|
||||||
ok = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
volume_mounts.push_back(std::move(*resolved));
|
|
||||||
}
|
|
||||||
|
|
||||||
auto resolved_env = resolve_env_specs(env_specs);
|
|
||||||
if (!resolved_env) {
|
|
||||||
ok = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Falls back to the image's own declared user (config.User) when --user wasn't
|
|
||||||
// given on the command line, rather than always defaulting to root.
|
|
||||||
std::optional<std::string> effective_user = user;
|
|
||||||
std::optional<std::string> effective_group = group;
|
|
||||||
if (!effective_user && config && !config->user.empty()) {
|
|
||||||
effective_user = config->user;
|
|
||||||
effective_group = config->group.empty() ? std::nullopt : std::optional<std::string>(config->group);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<ResolvedUser> resolved_user;
|
|
||||||
if (effective_user) {
|
|
||||||
resolved_user = resolve_user_and_group(*effective_user, effective_group, mounted->merged_path);
|
|
||||||
if (!resolved_user) {
|
|
||||||
ok = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::string> command = requested_command;
|
|
||||||
if (command.empty()) {
|
|
||||||
command = (config && !config->command.empty()) ? config->command
|
|
||||||
: std::vector<std::string>{"/bin/sh"};
|
|
||||||
}
|
|
||||||
|
|
||||||
std::function<void(pid_t)> on_bwrap_pid_known;
|
|
||||||
if (daemonize_flag) {
|
|
||||||
on_bwrap_pid_known = [&](pid_t pid) { report_daemon_started(container_name, pid); };
|
|
||||||
}
|
|
||||||
|
|
||||||
int exit_code = -1;
|
|
||||||
if (ok) {
|
|
||||||
exit_code = run_bwrap(mounted->merged_path, command, use_nsenter, volume_mounts, resolved_user, hostname,
|
|
||||||
container_name, *resolved_env, on_bwrap_pid_known);
|
|
||||||
if (exit_code < 0) {
|
|
||||||
spdlog::error("failed to run bwrap");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!unmount_layer(mounted->top_layer_id)) {
|
|
||||||
spdlog::error("failed to unmount layer {}", mounted->top_layer_id);
|
|
||||||
}
|
|
||||||
if (!cleanup_layer_chain(mounted->top_layer_id)) {
|
|
||||||
spdlog::error("failed to clean up layer {}", mounted->top_layer_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (!ok || exit_code < 0) ? 1 : exit_code;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
int main(int argc, char* argv[]) {
|
||||||
spdlog::cfg::load_env_levels();
|
spdlog::cfg::load_env_levels();
|
||||||
@@ -676,237 +35,10 @@ int main(int argc, char* argv[]) {
|
|||||||
apply_log_level(*config->log_level);
|
apply_log_level(*config->log_level);
|
||||||
}
|
}
|
||||||
|
|
||||||
Mode mode = Mode::none;
|
ParsedArgs args;
|
||||||
std::string mode_arg;
|
if (auto exit_code = parse_args(argc, argv, args)) {
|
||||||
bool disable_nsenter = false;
|
return *exit_code;
|
||||||
bool daemonize_flag = false;
|
|
||||||
std::optional<std::string> user_flag;
|
|
||||||
std::optional<std::string> group_flag;
|
|
||||||
std::optional<std::string> hostname_flag;
|
|
||||||
std::vector<std::pair<std::string, std::string>> volume_specs;
|
|
||||||
std::vector<EnvSpec> env_specs;
|
|
||||||
|
|
||||||
opterr = 0;
|
|
||||||
int opt;
|
|
||||||
while ((opt = getopt_long(argc, argv, ":hVtm:u:r:c:nl:v:i:e:D", long_options.data(), nullptr)) != -1) {
|
|
||||||
switch (opt) {
|
|
||||||
case 'h':
|
|
||||||
print_usage(argv[0]);
|
|
||||||
return 0;
|
|
||||||
case 'V':
|
|
||||||
print_version();
|
|
||||||
return 0;
|
|
||||||
case 't':
|
|
||||||
case 'm':
|
|
||||||
case 'u':
|
|
||||||
case 'r':
|
|
||||||
case 'c':
|
|
||||||
case 'l':
|
|
||||||
case 'i':
|
|
||||||
case 'e':
|
|
||||||
case options::list_volumes:
|
|
||||||
case options::delete_volume:
|
|
||||||
case options::delete_volume_full:
|
|
||||||
case options::list_processes:
|
|
||||||
case options::clean_processes: {
|
|
||||||
Mode requested;
|
|
||||||
switch (opt) {
|
|
||||||
case 't':
|
|
||||||
requested = Mode::test;
|
|
||||||
break;
|
|
||||||
case 'm':
|
|
||||||
requested = Mode::mount;
|
|
||||||
break;
|
|
||||||
case 'u':
|
|
||||||
requested = Mode::unmount;
|
|
||||||
break;
|
|
||||||
case 'r':
|
|
||||||
requested = Mode::run;
|
|
||||||
break;
|
|
||||||
case 'c':
|
|
||||||
requested = Mode::cleanup;
|
|
||||||
break;
|
|
||||||
case 'l':
|
|
||||||
requested = Mode::list_images;
|
|
||||||
break;
|
|
||||||
case 'i':
|
|
||||||
requested = Mode::inspect;
|
|
||||||
break;
|
|
||||||
case 'e':
|
|
||||||
requested = Mode::exec;
|
|
||||||
break;
|
|
||||||
case options::list_volumes:
|
|
||||||
requested = Mode::list_volumes;
|
|
||||||
break;
|
|
||||||
case options::delete_volume:
|
|
||||||
requested = Mode::delete_volume;
|
|
||||||
break;
|
|
||||||
case options::delete_volume_full:
|
|
||||||
requested = Mode::delete_volume_full;
|
|
||||||
break;
|
|
||||||
case options::list_processes:
|
|
||||||
requested = Mode::list_processes;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
requested = Mode::clean_processes;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (mode != Mode::none && mode != requested) {
|
|
||||||
spdlog::error("multiple actions specified");
|
|
||||||
print_usage(argv[0]);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
mode = requested;
|
|
||||||
if (optarg) {
|
|
||||||
mode_arg = optarg;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'v': {
|
|
||||||
// -v/--volume takes two tokens: optarg (the name or host path) plus
|
|
||||||
// the immediately following argv entry (the directory, or, with -r,
|
|
||||||
// the container path). Consumed manually (rather than via getopt's
|
|
||||||
// own required_argument) so -v can repeat with -r -- see main()'s
|
|
||||||
// post-loop handling for how the two uses are told apart.
|
|
||||||
if (optind >= argc || (argv[optind][0] == '-' && argv[optind][1] != '\0')) {
|
|
||||||
spdlog::error("--volume requires a name/path and a directory or container path");
|
|
||||||
print_usage(argv[0]);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
volume_specs.emplace_back(optarg, argv[optind]);
|
|
||||||
++optind;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'n':
|
|
||||||
disable_nsenter = true;
|
|
||||||
break;
|
|
||||||
case 'D':
|
|
||||||
daemonize_flag = true;
|
|
||||||
break;
|
|
||||||
case options::log_level:
|
|
||||||
if (!apply_log_level(optarg)) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case options::user:
|
|
||||||
user_flag = optarg;
|
|
||||||
break;
|
|
||||||
case options::group:
|
|
||||||
group_flag = optarg;
|
|
||||||
break;
|
|
||||||
case options::hostname:
|
|
||||||
hostname_flag = optarg;
|
|
||||||
break;
|
|
||||||
case options::env:
|
|
||||||
env_specs.push_back({false, optarg});
|
|
||||||
break;
|
|
||||||
case options::env_file:
|
|
||||||
env_specs.push_back({true, optarg});
|
|
||||||
break;
|
|
||||||
case ':':
|
|
||||||
spdlog::error("option requires an argument: -{}", static_cast<char>(optopt));
|
|
||||||
print_usage(argv[0]);
|
|
||||||
return 1;
|
|
||||||
case '?':
|
|
||||||
default:
|
|
||||||
spdlog::error("unrecognized option");
|
|
||||||
print_usage(argv[0]);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!volume_specs.empty() && mode != Mode::run) {
|
return dispatch_command(args, config_path, *config);
|
||||||
if (mode != Mode::none) {
|
|
||||||
spdlog::error("--volume can only be used standalone or together with --run");
|
|
||||||
print_usage(argv[0]);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (volume_specs.size() > 1) {
|
|
||||||
spdlog::error("--volume can only be used once outside of --run");
|
|
||||||
print_usage(argv[0]);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
mode = Mode::volume;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode == Mode::none) {
|
|
||||||
print_usage(argv[0]);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (mode != Mode::run && mode != Mode::exec && optind != argc) {
|
|
||||||
print_usage(argv[0]);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (group_flag && !user_flag) {
|
|
||||||
spdlog::error("--group requires --user");
|
|
||||||
print_usage(argv[0]);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode == Mode::test) {
|
|
||||||
return run_tests();
|
|
||||||
}
|
|
||||||
if (mode == Mode::unmount) {
|
|
||||||
return unmount_image(mode_arg);
|
|
||||||
}
|
|
||||||
if (mode == Mode::cleanup) {
|
|
||||||
return cleanup_image(mode_arg);
|
|
||||||
}
|
|
||||||
if (mode == Mode::list_images) {
|
|
||||||
return list_images_command(mode_arg);
|
|
||||||
}
|
|
||||||
if (mode == Mode::inspect) {
|
|
||||||
return inspect_image_command(mode_arg);
|
|
||||||
}
|
|
||||||
if (mode == Mode::volume) {
|
|
||||||
return create_volume_command(volume_specs.front().first, volume_specs.front().second, config_path,
|
|
||||||
*config);
|
|
||||||
}
|
|
||||||
if (mode == Mode::list_volumes) {
|
|
||||||
return list_volumes_command(*config);
|
|
||||||
}
|
|
||||||
if (mode == Mode::delete_volume || mode == Mode::delete_volume_full) {
|
|
||||||
return delete_volume_command(mode_arg, config_path, *config, mode == Mode::delete_volume_full);
|
|
||||||
}
|
|
||||||
if (mode == Mode::list_processes) {
|
|
||||||
return list_processes_command();
|
|
||||||
}
|
|
||||||
if (mode == Mode::clean_processes) {
|
|
||||||
return clean_processes_command();
|
|
||||||
}
|
|
||||||
if (mode == Mode::exec) {
|
|
||||||
std::vector<std::string> command(argv + optind, argv + argc);
|
|
||||||
if (command.empty()) {
|
|
||||||
spdlog::error("--exec requires a command to run");
|
|
||||||
print_usage(argv[0]);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
char* end = nullptr;
|
|
||||||
long parsed = std::strtol(mode_arg.c_str(), &end, 10);
|
|
||||||
if (mode_arg.empty() || !end || *end != '\0' || parsed <= 0) {
|
|
||||||
spdlog::error("invalid pid: {}", mode_arg);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return exec_in_session(static_cast<pid_t>(parsed), command);
|
|
||||||
}
|
|
||||||
if (mode == Mode::run) {
|
|
||||||
std::vector<std::string> command(argv + optind, argv + argc);
|
|
||||||
// As root, containers-storage mount doesn't need to reexec into a private
|
|
||||||
// user namespace to gain privilege, so the mount is already directly
|
|
||||||
// visible; nsenter into it then fails ("reassociate to namespace 'ns/user'
|
|
||||||
// failed: Invalid argument") since we're already in that same namespace.
|
|
||||||
bool use_nsenter = !disable_nsenter && geteuid() != 0;
|
|
||||||
if (geteuid() == 0 && !disable_nsenter) {
|
|
||||||
spdlog::debug("running as root; skipping nsenter (the mount is already directly visible)");
|
|
||||||
}
|
|
||||||
return run_container(mode_arg, command, use_nsenter, user_flag, group_flag, hostname_flag, volume_specs,
|
|
||||||
env_specs, daemonize_flag, *config);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto mounted = mount_image(mode_arg);
|
|
||||||
if (!mounted) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
fmt::print("mounted image at: {} (layer {})\n", mounted->merged_path, mounted->top_layer_id);
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// Copyright (C) 2026 Viorel Munteanu
|
||||||
|
//
|
||||||
|
// This program is free software; you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation; either version 2 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License along
|
||||||
|
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
#include "self_test.h"
|
||||||
|
|
||||||
|
#include <fmt/core.h>
|
||||||
|
|
||||||
|
#include "bwrap.h"
|
||||||
|
|
||||||
|
int run_self_tests() {
|
||||||
|
for (const auto& arg : detect_bwrap_unshare_args()) {
|
||||||
|
fmt::print("{}\n", arg);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Copyright (C) 2026 Viorel Munteanu
|
||||||
|
//
|
||||||
|
// This program is free software; you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation; either version 2 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License along
|
||||||
|
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Implements -t/--test, this project's own built-in self-test mode (distinct
|
||||||
|
// from the Meson-driven fixture smoke test under tests/). Currently just
|
||||||
|
// reports which bwrap --unshare-xxx flags the running kernel supports (see
|
||||||
|
// detect_bwrap_unshare_args(), bwrap.h) -- a placeholder for real tests.
|
||||||
|
int run_self_tests();
|
||||||
Reference in New Issue
Block a user