diff --git a/CLAUDE.md b/CLAUDE.md index 32c9e4f..3bf7473 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,11 @@ status); this file stays the dense, file-by-file reference. Still early-stage. Source layout (all under `src/`): - `main.cpp` — CLI entry point, dependency checks, orchestration (`mount_image()`, `run_container()`, `cleanup_image()`, `unmount_image()`, `list_images_command()`, - `create_volume_command()`, `list_volumes_command()`, `delete_volume_command()`). + `inspect_image_command()`, `create_volume_command()`, `list_volumes_command()`, + `delete_volume_command()`). `inspect_image_command()` implements `-i/--inspect + `: prints every `OciImageConfig` field (user/group, exposed ports, env, + volumes, default command) without mounting or running the image — extend it + whenever `OciImageConfig` gains a new field (see `oci_image.{h,cpp}` below). `run_container()` unconditionally calls `read_oci_image_config()` and reuses the result for two independent defaults: the command to run (`Entrypoint ++ Cmd`) when none is given on the command line, and, when `--user` wasn't given, the sandboxed @@ -41,8 +45,9 @@ Source layout (all under `src/`): and extracts `User` (split on `:` into `OciImageConfig::user`/`group`), `ExposedPorts`, `Env`, `Volumes`, and the effective default command (`Entrypoint ++ Cmd`). `user`/`group` and the default command are consumed by - `-r/--run` (see `main.cpp` above) — `ExposedPorts`/`Env`/`Volumes` are still just - captured for when networking/volumes are implemented. + `-r/--run`, and every field is displayed by `-i/--inspect` (see `main.cpp` above) + — `ExposedPorts`/`Env`/`Volumes` are otherwise still just captured for when + networking/volumes are implemented. - `containers_storage.{h,cpp}` — wraps the `containers-storage` CLI (`import-layer`, `mount`, `unmount`, `layer --json`, `delete-layer`), forcing `fuse-overlayfs` as the overlay `mount_program`. `cleanup_layer_chain()` walks a layer's parent chain @@ -158,9 +163,9 @@ Build directory is `buildDir/` (already configured). (see `priv_drop_helper.cpp` in "Project state") - Run the executable: `./buildDir/slocker-lite -m ` (see `--help` for the full flag list: `-m/--mount`, `-r/--run`, `-u/--umount`, `-c/--cleanup`, - `-l/--list-images`, `-n/--no-nsenter`, `--user`, `--group`, `-v/--volume`, - `--list-volumes`, `--delete-volume`, `--delete-volume-full`, `-t/--test`, - `--log-level`, `-h/--help`, `-V/--version`) + `-l/--list-images`, `-i/--inspect`, `-n/--no-nsenter`, `--user`, `--group`, + `-v/--volume`, `--list-volumes`, `--delete-volume`, `--delete-volume-full`, + `-t/--test`, `--log-level`, `-h/--help`, `-V/--version`) - Run tests: `meson test -C buildDir` ## Code style diff --git a/README.md b/README.md index 9a22710..8b8ed51 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ slocker-lite -r|--run [-- [args...]] slocker-lite -u|--umount slocker-lite -c|--cleanup slocker-lite -l|--list-images +slocker-lite -i|--inspect slocker-lite -v|--volume slocker-lite --list-volumes slocker-lite --delete-volume @@ -76,6 +77,7 @@ slocker-lite -V|--version | `--user ` | With `--run`, run the command as this user (name or numeric uid) instead of the image's own declared user (or root, if it declares none). Resolved against the image's own `/etc/passwd`. Only takes effect when `--run` executes as root. | | `--group ` | With `--user`, use this group (name or numeric gid) instead of the user's primary group. | | `-l, --list-images ` | List OCI Image Layout tars (`*.tar`, `*.tar.*`) found directly in ``, with their `name:tag`. | +| `-i, --inspect ` | Print an image's declared user, exposed ports, env, volumes, and default command, without mounting or running it. | | `-v, --volume ` | Create a named volume mapped to a host directory (created if missing), recorded in the config file's `volumes` section. Fails if the name or directory is already used by an existing volume. | | `--list-volumes` | List all named volumes (see `-v/--volume`) with their host directory. | | `--delete-volume ` | Remove a named volume from the config. The host directory is left untouched. | @@ -103,6 +105,9 @@ sudo ./buildDir/slocker-lite -r myimage.tar --user git # List every OCI image tar in a directory ./buildDir/slocker-lite -l ./images +# Inspect an image's declared config without mounting or running it +./buildDir/slocker-lite -i myimage.tar + # Create a named volume backed by a host directory ./buildDir/slocker-lite -v mydata ~/slocker-volumes/mydata diff --git a/src/main.cpp b/src/main.cpp index b8f4611..be71627 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include @@ -52,7 +53,8 @@ enum class Mode { kVolume, kListVolumes, kDeleteVolume, - kDeleteVolumeFull + kDeleteVolumeFull, + kInspect }; // --log-level/--user/--group/--list-volumes/--delete-volume[-full] have no short @@ -66,7 +68,7 @@ constexpr int kListVolumesOpt = 259; constexpr int kDeleteVolumeOpt = 260; constexpr int kDeleteVolumeFullOpt = 261; -constexpr std::array kLongOptions = {{ +constexpr std::array kLongOptions = {{ {"help", no_argument, nullptr, 'h'}, {"version", no_argument, nullptr, 'V'}, {"test", no_argument, nullptr, 't'}, @@ -83,6 +85,7 @@ constexpr std::array kLongOptions = {{ {"list-volumes", no_argument, nullptr, kListVolumesOpt}, {"delete-volume", required_argument, nullptr, kDeleteVolumeOpt}, {"delete-volume-full", required_argument, nullptr, kDeleteVolumeFullOpt}, + {"inspect", required_argument, nullptr, 'i'}, {nullptr, 0, nullptr, 0}, }}; @@ -93,6 +96,7 @@ void print_usage(const char* prog) { " {0} -u|--umount \n" " {0} -c|--cleanup \n" " {0} -l|--list-images \n" + " {0} -i|--inspect \n" " {0} -v|--volume \n" " {0} --list-volumes\n" " {0} --delete-volume \n" @@ -128,6 +132,9 @@ void print_usage(const char* prog) { " gid) instead of the user's own primary group\n" " -l, --list-images list OCI Image Layout tars (*.tar, *.tar.*) found\n" " directly in , with their name:tag\n" + " -i, --inspect print an image's declared user, exposed ports,\n" + " env, volumes, and default command, without\n" + " mounting or running it\n" " -v, --volume create a named volume mapped to a host directory\n" " (created if missing), recorded in the config\n" " file's volumes section\n" @@ -291,6 +298,57 @@ int list_images_command(const std::filesystem::path& dir) { 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::kTcp ? "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) { std::filesystem::path resolved = std::filesystem::absolute(directory).lexically_normal(); @@ -452,7 +510,7 @@ int main(int argc, char* argv[]) { opterr = 0; int opt; - while ((opt = getopt_long(argc, argv, ":hVtm:u:r:c:nl:v:", kLongOptions.data(), nullptr)) != -1) { + while ((opt = getopt_long(argc, argv, ":hVtm:u:r:c:nl:v:i:", kLongOptions.data(), nullptr)) != -1) { switch (opt) { case 'h': print_usage(argv[0]); @@ -467,6 +525,7 @@ int main(int argc, char* argv[]) { case 'c': case 'l': case 'v': + case 'i': case kListVolumesOpt: case kDeleteVolumeOpt: case kDeleteVolumeFullOpt: { @@ -493,6 +552,9 @@ int main(int argc, char* argv[]) { case 'v': requested = Mode::kVolume; break; + case 'i': + requested = Mode::kInspect; + break; case kListVolumesOpt: requested = Mode::kListVolumes; break; @@ -572,6 +634,9 @@ int main(int argc, char* argv[]) { if (mode == Mode::kListImages) { return list_images_command(mode_arg); } + if (mode == Mode::kInspect) { + return inspect_image_command(mode_arg); + } if (mode == Mode::kVolume) { return create_volume_command(mode_arg, argv[optind], config_path, *config); }