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:
+41
-2
@@ -82,7 +82,7 @@ constexpr int umount = 280;
|
|||||||
constexpr int cleanup = 281;
|
constexpr int cleanup = 281;
|
||||||
} // namespace options
|
} // namespace options
|
||||||
|
|
||||||
constexpr std::array<struct option, 40> long_options = {{
|
constexpr std::array<struct option, 42> long_options = {{
|
||||||
{"help", no_argument, nullptr, 'h'},
|
{"help", no_argument, nullptr, 'h'},
|
||||||
{"version", no_argument, nullptr, 'V'},
|
{"version", no_argument, nullptr, 'V'},
|
||||||
{"test", no_argument, nullptr, 't'},
|
{"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},
|
{"delete-network-full", required_argument, nullptr, options::delete_network_full},
|
||||||
{"port-forward", required_argument, nullptr, 'p'},
|
{"port-forward", required_argument, nullptr, 'p'},
|
||||||
{"no-dns", no_argument, nullptr, options::no_dns},
|
{"no-dns", no_argument, nullptr, options::no_dns},
|
||||||
|
{"up", required_argument, nullptr, 'u'},
|
||||||
|
{"down", required_argument, nullptr, 'd'},
|
||||||
{nullptr, 0, nullptr, 0},
|
{nullptr, 0, nullptr, 0},
|
||||||
}};
|
}};
|
||||||
|
|
||||||
@@ -148,6 +150,8 @@ void print_usage(const char* prog) {
|
|||||||
" {0} --delete-network-full <name>\n"
|
" {0} --delete-network-full <name>\n"
|
||||||
" {0} --list-processes\n"
|
" {0} --list-processes\n"
|
||||||
" {0} --clean-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} [-c|--config-file <path>] -w|--write-config\n"
|
||||||
" {0} -t|--test [-- <catch-command-line-options>]\n"
|
" {0} -t|--test [-- <catch-command-line-options>]\n"
|
||||||
" {0} -h|--help\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"
|
" --clean-processes remove stale pid files (see --list-processes)\n"
|
||||||
" left behind by sessions that are no longer\n"
|
" left behind by sessions that are no longer\n"
|
||||||
" running\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"
|
" -w, --write-config write a complete *global* config file (creating\n"
|
||||||
" it, and its parent directory, if missing),\n"
|
" it, and its parent directory, if missing),\n"
|
||||||
" filling in every global option's current or\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;
|
optind = 0;
|
||||||
opterr = 0;
|
opterr = 0;
|
||||||
int opt;
|
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) {
|
switch (opt) {
|
||||||
case 'h':
|
case 'h':
|
||||||
print_usage(argv[0]);
|
print_usage(argv[0]);
|
||||||
@@ -502,6 +517,30 @@ std::optional<int> parse_args(int argc, char* argv[], ParsedArgs& out) {
|
|||||||
++optind;
|
++optind;
|
||||||
break;
|
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':
|
case 'n':
|
||||||
// -n/--network takes a single token (the name), always via
|
// -n/--network takes a single token (the name), always via
|
||||||
// getopt's own required_argument -- repeatable, so it's
|
// getopt's own required_argument -- repeatable, so it's
|
||||||
|
|||||||
+14
-1
@@ -47,7 +47,9 @@ enum class Mode {
|
|||||||
network,
|
network,
|
||||||
list_networks,
|
list_networks,
|
||||||
delete_network,
|
delete_network,
|
||||||
delete_network_full
|
delete_network_full,
|
||||||
|
compose_up,
|
||||||
|
compose_down
|
||||||
};
|
};
|
||||||
|
|
||||||
// Everything parse_args() extracts from argv, ready to hand to
|
// 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
|
// (possibly -c-sourced) global config's own log-level -- see
|
||||||
// apply_log_level()'s own doc comment below for why this matters.
|
// apply_log_level()'s own doc comment below for why this matters.
|
||||||
bool log_level_flag_given = false;
|
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)
|
// Validates and applies a log-level name (trace/debug/info/warn/error/critical/off)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
#include "bwrap.h"
|
#include "bwrap.h"
|
||||||
|
#include "compose_file.h"
|
||||||
#include "containers_storage.h"
|
#include "containers_storage.h"
|
||||||
#include "daemonize.h"
|
#include "daemonize.h"
|
||||||
#include "env_spec.h"
|
#include "env_spec.h"
|
||||||
@@ -576,6 +577,60 @@ int write_config_command(const std::filesystem::path& config_path, const AppConf
|
|||||||
return 0;
|
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,
|
int run_container(const std::filesystem::path& image_tar,
|
||||||
const std::vector<std::string>& requested_command, bool use_nsenter,
|
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>& 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);
|
return delete_network_command(args.mode_arg, config, false);
|
||||||
case Mode::delete_network_full:
|
case Mode::delete_network_full:
|
||||||
return delete_network_command(args.mode_arg, config, true);
|
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: {
|
case Mode::run: {
|
||||||
// As root, containers-storage mount doesn't need to reexec into a private
|
// As root, containers-storage mount doesn't need to reexec into a private
|
||||||
// user namespace to gain privilege, so the mount is already directly
|
// user namespace to gain privilege, so the mount is already directly
|
||||||
|
|||||||
@@ -249,3 +249,48 @@ TEST_CASE("parse_args: --log-level sets log_level_flag_given", "[unit]") {
|
|||||||
REQUIRE_FALSE(with_level.exit_code.has_value());
|
REQUIRE_FALSE(with_level.exit_code.has_value());
|
||||||
CHECK(with_level.args.log_level_flag_given);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user