Add -u/--up and -d/--down, wired to compose-parser stub commands

Both take a required OCI images directory plus an optional compose file
name (defaulting to "compose.yaml", resolved relative to the cwd) via the
same manual two-token consumption -v/--volume already uses, just with the
second token optional. The stub commands aren't no-ops: they resolve the
images directory, load and validate the compose file through the existing
load_compose_file()/validate_compose_external_state(), and print a summary
-- confirming the CLI wiring and parser work end to end -- before logging
that actual orchestration isn't implemented yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
2026-09-07 10:45:48 +00:00
parent a626ff2cdd
commit f354463b40
4 changed files with 159 additions and 3 deletions
+41 -2
View File
@@ -82,7 +82,7 @@ constexpr int umount = 280;
constexpr int cleanup = 281;
} // namespace options
constexpr std::array<struct option, 40> long_options = {{
constexpr std::array<struct option, 42> long_options = {{
{"help", no_argument, nullptr, 'h'},
{"version", no_argument, nullptr, 'V'},
{"test", no_argument, nullptr, 't'},
@@ -122,6 +122,8 @@ constexpr std::array<struct option, 40> long_options = {{
{"delete-network-full", required_argument, nullptr, options::delete_network_full},
{"port-forward", required_argument, nullptr, 'p'},
{"no-dns", no_argument, nullptr, options::no_dns},
{"up", required_argument, nullptr, 'u'},
{"down", required_argument, nullptr, 'd'},
{nullptr, 0, nullptr, 0},
}};
@@ -148,6 +150,8 @@ void print_usage(const char* prog) {
" {0} --delete-network-full <name>\n"
" {0} --list-processes\n"
" {0} --clean-processes\n"
" {0} -u|--up <images-directory> [<compose-file-name>]\n"
" {0} -d|--down <images-directory> [<compose-file-name>]\n"
" {0} [-c|--config-file <path>] -w|--write-config\n"
" {0} -t|--test [-- <catch-command-line-options>]\n"
" {0} -h|--help\n"
@@ -297,6 +301,17 @@ void print_usage(const char* prog) {
" --clean-processes remove stale pid files (see --list-processes)\n"
" left behind by sessions that are no longer\n"
" running\n"
" -u, --up <images-directory> [<compose-file-name>]\n"
" load and validate a Compose file (default\n"
" \"compose.yaml\", resolved relative to the\n"
" current directory) against <images-directory>\n"
" (where its services' images are expected to\n"
" live) -- orchestration itself (actually\n"
" starting the services) isn't implemented yet\n"
" -d, --down <images-directory> [<compose-file-name>]\n"
" same file/directory resolution as -u/--up --\n"
" orchestration itself (actually stopping the\n"
" services) isn't implemented yet\n"
" -w, --write-config write a complete *global* config file (creating\n"
" it, and its parent directory, if missing),\n"
" filling in every global option's current or\n"
@@ -393,7 +408,7 @@ std::optional<int> parse_args(int argc, char* argv[], ParsedArgs& out) {
optind = 0;
opterr = 0;
int opt;
while ((opt = getopt_long(argc, argv, ":hVtc:r:l:v:i:x:Dwn:p:", long_options.data(), nullptr)) != -1) {
while ((opt = getopt_long(argc, argv, ":hVtc:r:l:v:i:x:Dwn:p:u:d:", long_options.data(), nullptr)) != -1) {
switch (opt) {
case 'h':
print_usage(argv[0]);
@@ -502,6 +517,30 @@ std::optional<int> parse_args(int argc, char* argv[], ParsedArgs& out) {
++optind;
break;
}
case 'u':
case 'd': {
// -u/--up and -d/--down both take the OCI images directory
// (optarg, required) plus an optional second token naming the
// compose YAML file to load, resolved relative to the cwd --
// same manual second-token consumption -v/--volume already
// uses above, except this second token is optional: only
// consumed when present and not itself the next flag (the
// same "looks like -x" guard -v's own required second token
// uses to detect it was omitted).
Mode requested = (opt == 'u') ? Mode::compose_up : Mode::compose_down;
if (out.mode != Mode::none && out.mode != requested) {
spdlog::error("multiple actions specified");
print_usage(argv[0]);
return 1;
}
out.mode = requested;
out.compose_images_directory = optarg;
if (optind < argc && !(argv[optind][0] == '-' && argv[optind][1] != '\0')) {
out.compose_file_name = argv[optind];
++optind;
}
break;
}
case 'n':
// -n/--network takes a single token (the name), always via
// getopt's own required_argument -- repeatable, so it's
+14 -1
View File
@@ -47,7 +47,9 @@ enum class Mode {
network,
list_networks,
delete_network,
delete_network_full
delete_network_full,
compose_up,
compose_down
};
// Everything parse_args() extracts from argv, ready to hand to
@@ -120,6 +122,17 @@ struct ParsedArgs {
// (possibly -c-sourced) global config's own log-level -- see
// apply_log_level()'s own doc comment below for why this matters.
bool log_level_flag_given = false;
// -u/--up <images-directory> [<compose-file-name>] and -d/--down
// <images-directory> [<compose-file-name>]: `compose_images_directory`
// (optarg) is required; the second, optional token names the compose
// YAML file to load -- resolved relative to the current working
// directory, same as any other plain file argument on this CLI (e.g.
// -r/--run <image.tar>), *not* relative to the images directory.
// Defaults to "compose.yaml" when omitted. Consumed the same manual
// second-token technique -v/--volume already uses (cli_args.cpp), just
// with the second token optional rather than required.
std::optional<std::string> compose_images_directory;
std::string compose_file_name = "compose.yaml";
};
// Validates and applies a log-level name (trace/debug/info/warn/error/critical/off)
+59
View File
@@ -33,6 +33,7 @@
#include <spdlog/spdlog.h>
#include "bwrap.h"
#include "compose_file.h"
#include "containers_storage.h"
#include "daemonize.h"
#include "env_spec.h"
@@ -576,6 +577,60 @@ int write_config_command(const std::filesystem::path& config_path, const AppConf
return 0;
}
// Shared by compose_up_command()/compose_down_command() below: both need
// the exact same resolve-images-directory + load_compose_file() +
// validate_compose_external_state() step before doing anything
// mode-specific -- which, for now, is nothing at all; see each caller's own
// "not implemented yet" message. `compose_file_name` is resolved relative
// to the current working directory (cli_args.h's own doc comment on
// ParsedArgs::compose_file_name), independent of `images_directory`.
std::optional<ComposeFile> load_and_validate_compose(const std::string& images_directory,
const std::string& compose_file_name, AppConfig& config) {
if (!std::filesystem::is_directory(images_directory)) {
spdlog::error("images directory does not exist or isn't a directory: {}", images_directory);
return std::nullopt;
}
auto compose_path = std::filesystem::absolute(compose_file_name);
auto compose = load_compose_file(compose_path);
if (!compose) {
return std::nullopt;
}
if (!validate_compose_external_state(*compose, config)) {
return std::nullopt;
}
fmt::print("compose file {} parsed and validated: {} service(s), {} network(s), {} volume(s)\n",
compose_path.string(), compose->services.size(), compose->networks.size(), compose->volumes.size());
fmt::print("images directory: {}\n", images_directory);
return compose;
}
// Stub for -u/--up: confirms the CLI wiring and the existing parser/
// validator (compose_file.h) work end to end against a real compose file,
// but doesn't actually start anything yet -- see CLAUDE.md's own "Mutable
// global state and multi-container support" note for the planned
// one-forked-process-per-service orchestration model this will eventually
// drive.
int compose_up_command(const std::string& images_directory, const std::string& compose_file_name,
AppConfig& config) {
if (!load_and_validate_compose(images_directory, compose_file_name, config)) {
return 1;
}
spdlog::warn("compose orchestration (-u/--up) isn't implemented yet -- nothing was actually started");
return 0;
}
// Stub for -d/--down -- same idea as compose_up_command() above.
int compose_down_command(const std::string& images_directory, const std::string& compose_file_name,
AppConfig& config) {
if (!load_and_validate_compose(images_directory, compose_file_name, config)) {
return 1;
}
spdlog::warn("compose orchestration (-d/--down) isn't implemented yet -- nothing was actually stopped");
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,
@@ -913,6 +968,10 @@ int dispatch_command(const ParsedArgs& args, const std::filesystem::path& config
return delete_network_command(args.mode_arg, config, false);
case Mode::delete_network_full:
return delete_network_command(args.mode_arg, config, true);
case Mode::compose_up:
return compose_up_command(*args.compose_images_directory, args.compose_file_name, config);
case Mode::compose_down:
return compose_down_command(*args.compose_images_directory, args.compose_file_name, config);
case Mode::run: {
// As root, containers-storage mount doesn't need to reexec into a private
// user namespace to gain privilege, so the mount is already directly
+45
View File
@@ -249,3 +249,48 @@ TEST_CASE("parse_args: --log-level sets log_level_flag_given", "[unit]") {
REQUIRE_FALSE(with_level.exit_code.has_value());
CHECK(with_level.args.log_level_flag_given);
}
TEST_CASE("parse_args: -u/--up defaults compose_file_name to compose.yaml when omitted", "[unit]") {
auto result = run_parse({"-u", "/var/lib/images"});
REQUIRE_FALSE(result.exit_code.has_value());
CHECK(result.args.mode == Mode::compose_up);
REQUIRE(result.args.compose_images_directory.has_value());
CHECK(*result.args.compose_images_directory == "/var/lib/images");
CHECK(result.args.compose_file_name == "compose.yaml");
}
TEST_CASE("parse_args: -u accepts an explicit second token as compose_file_name", "[unit]") {
auto result = run_parse({"-u", "/var/lib/images", "my-stack.yaml"});
REQUIRE_FALSE(result.exit_code.has_value());
CHECK(result.args.mode == Mode::compose_up);
CHECK(*result.args.compose_images_directory == "/var/lib/images");
CHECK(result.args.compose_file_name == "my-stack.yaml");
}
TEST_CASE("parse_args: -u doesn't swallow a following flag as compose_file_name", "[unit]") {
auto result = run_parse({"-u", "/var/lib/images", "--log-level", "debug"});
REQUIRE_FALSE(result.exit_code.has_value());
CHECK(result.args.mode == Mode::compose_up);
CHECK(*result.args.compose_images_directory == "/var/lib/images");
// The default, since "--log-level" looks like a flag, not a filename.
CHECK(result.args.compose_file_name == "compose.yaml");
CHECK(result.args.log_level_flag_given);
}
TEST_CASE("parse_args: -d/--down works the same way as -u/--up", "[unit]") {
auto default_name = run_parse({"--down", "/var/lib/images"});
REQUIRE_FALSE(default_name.exit_code.has_value());
CHECK(default_name.args.mode == Mode::compose_down);
CHECK(default_name.args.compose_file_name == "compose.yaml");
auto explicit_name = run_parse({"-d", "/var/lib/images", "my-stack.yaml"});
REQUIRE_FALSE(explicit_name.exit_code.has_value());
CHECK(explicit_name.args.mode == Mode::compose_down);
CHECK(explicit_name.args.compose_file_name == "my-stack.yaml");
}
TEST_CASE("parse_args: -u and -d together on the same command line is a parse error", "[unit]") {
auto result = run_parse({"-u", "/var/lib/images", "-d", "/var/lib/images"});
REQUIRE(result.exit_code.has_value());
CHECK(*result.exit_code == 1);
}