Add --delete-volume and --delete-volume-full

--delete-volume <name> removes a volume's config entry only.
--delete-volume-full <name> also recursively deletes its host
directory -- fails before touching the config if that deletion errors,
so a failed directory delete never silently drops the config entry;
warns instead of failing if the directory was already gone.

Both are long-only, following the --list-volumes precedent, and slot
into the existing multi-mode getopt dispatch the same way -v/--volume
already does.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
2026-08-21 13:21:46 +00:00
parent afca0f7ce3
commit abf268e950
3 changed files with 97 additions and 17 deletions
+73 -8
View File
@@ -41,18 +41,32 @@ namespace {
constexpr std::array<std::string_view, 2> kRequiredTools = {"containers-storage", "bwrap"};
enum class Mode { kNone, kMount, kUnmount, kTest, kRun, kCleanup, kListImages, kVolume, kListVolumes };
enum class Mode {
kNone,
kMount,
kUnmount,
kTest,
kRun,
kCleanup,
kListImages,
kVolume,
kListVolumes,
kDeleteVolume,
kDeleteVolumeFull
};
// --log-level/--user/--group/--list-volumes have no short form (--log-level's was
// freed up so -l could become --list-images; -u is already --umount; --list-volumes
// has no natural free letter left), so they need long-option vals outside the
// printable-char range short options use.
// --log-level/--user/--group/--list-volumes/--delete-volume[-full] 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.
constexpr int kLogLevelOpt = 256;
constexpr int kUserOpt = 257;
constexpr int kGroupOpt = 258;
constexpr int kListVolumesOpt = 259;
constexpr int kDeleteVolumeOpt = 260;
constexpr int kDeleteVolumeFullOpt = 261;
constexpr std::array<struct option, 15> kLongOptions = {{
constexpr std::array<struct option, 17> kLongOptions = {{
{"help", no_argument, nullptr, 'h'},
{"version", no_argument, nullptr, 'V'},
{"test", no_argument, nullptr, 't'},
@@ -67,6 +81,8 @@ constexpr std::array<struct option, 15> kLongOptions = {{
{"group", required_argument, nullptr, kGroupOpt},
{"volume", required_argument, nullptr, 'v'},
{"list-volumes", no_argument, nullptr, kListVolumesOpt},
{"delete-volume", required_argument, nullptr, kDeleteVolumeOpt},
{"delete-volume-full", required_argument, nullptr, kDeleteVolumeFullOpt},
{nullptr, 0, nullptr, 0},
}};
@@ -79,6 +95,8 @@ void print_usage(const char* prog) {
" {0} -l|--list-images <directory>\n"
" {0} -v|--volume <name> <directory>\n"
" {0} --list-volumes\n"
" {0} --delete-volume <name>\n"
" {0} --delete-volume-full <name>\n"
" {0} -t|--test\n"
" {0} -h|--help\n"
" {0} -V|--version\n"
@@ -115,6 +133,12 @@ void print_usage(const char* prog) {
" file's volumes section\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"
" -t, --test run the test suite\n"
" --log-level <level> set log verbosity (trace, debug, info, warn,\n"
" error, critical, off)\n"
@@ -325,6 +349,36 @@ int list_volumes_command(const AppConfig& config) {
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) {
@@ -413,7 +467,9 @@ int main(int argc, char* argv[]) {
case 'c':
case 'l':
case 'v':
case kListVolumesOpt: {
case kListVolumesOpt:
case kDeleteVolumeOpt:
case kDeleteVolumeFullOpt: {
Mode requested;
switch (opt) {
case 't':
@@ -437,9 +493,15 @@ int main(int argc, char* argv[]) {
case 'v':
requested = Mode::kVolume;
break;
default:
case kListVolumesOpt:
requested = Mode::kListVolumes;
break;
case kDeleteVolumeOpt:
requested = Mode::kDeleteVolume;
break;
default:
requested = Mode::kDeleteVolumeFull;
break;
}
if (mode != Mode::kNone && mode != requested) {
spdlog::error("multiple actions specified");
@@ -516,6 +578,9 @@ int main(int argc, char* argv[]) {
if (mode == Mode::kListVolumes) {
return list_volumes_command(*config);
}
if (mode == Mode::kDeleteVolume || mode == Mode::kDeleteVolumeFull) {
return delete_volume_command(mode_arg, config_path, *config, mode == Mode::kDeleteVolumeFull);
}
if (mode == Mode::kRun) {
std::vector<std::string> command(argv + optind, argv + argc);
// As root, containers-storage mount doesn't need to reexec into a private