Skip to content

Commit

Permalink
Merge pull request #3313 from didier-wenzek/feat/simplify-tedge-multi…
Browse files Browse the repository at this point in the history
…call

feat: Trigger tedge multicall with symlinks as well as sub-commands
  • Loading branch information
didier-wenzek authored Jan 8, 2025
2 parents 3d85d66 + c80b3d0 commit bd06fcb
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 33 deletions.
1 change: 1 addition & 0 deletions crates/core/tedge/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ license = { workspace = true }
homepage = { workspace = true }
repository = { workspace = true }
readme = "README.md"
default-run = "tedge"

[dependencies]
anstyle = { workspace = true }
Expand Down
26 changes: 22 additions & 4 deletions crates/core/tedge/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use c8y_firmware_plugin::FirmwarePluginOpt;
use c8y_remote_access_plugin::C8yRemoteAccessPluginOpt;
pub use connect::*;
use tedge_agent::AgentOpt;
use tedge_apt_plugin::AptCli;
use tedge_config::cli::CommonArgs;
use tedge_mapper::MapperOpt;
use tedge_watchdog::WatchdogOpt;
Expand Down Expand Up @@ -36,6 +37,7 @@ mod upload;
multicall(true),
)]
pub enum TEdgeOptMulticall {
/// Command line interface to interact with thin-edge.io
Tedge {
#[clap(subcommand)]
cmd: TEdgeOpt,
Expand All @@ -50,15 +52,18 @@ pub enum TEdgeOptMulticall {

#[derive(clap::Parser, Debug)]
pub enum Component {
TedgeMapper(MapperOpt),
C8yFirmwarePlugin(FirmwarePluginOpt),

C8yRemoteAccessPlugin(C8yRemoteAccessPluginOpt),

TedgeAgent(AgentOpt),

C8yFirmwarePlugin(FirmwarePluginOpt),
#[clap(alias = "apt")]
TedgeAptPlugin(AptCli),

TedgeWatchdog(WatchdogOpt),
TedgeMapper(MapperOpt),

C8yRemoteAccessPlugin(C8yRemoteAccessPluginOpt),
TedgeWatchdog(WatchdogOpt),

TedgeWrite(TedgeWriteOpt),
}
Expand Down Expand Up @@ -113,6 +118,15 @@ pub enum TEdgeOpt {
/// Publish a message on a topic and subscribe a topic.
#[clap(subcommand)]
Mqtt(mqtt::TEdgeMqttCli),

/// Run thin-edge services and plugins
Run(ComponentOpt),
}

#[derive(Debug, clap::Parser)]
pub struct ComponentOpt {
#[clap(subcommand)]
pub component: Component,
}

fn styles() -> clap::builder::Styles {
Expand Down Expand Up @@ -174,6 +188,10 @@ impl BuildCommand for TEdgeOpt {
TEdgeOpt::RefreshBridges => RefreshBridgesCmd::new(&context).map(Command::into_boxed),
TEdgeOpt::Mqtt(opt) => opt.build_command(context),
TEdgeOpt::Reconnect(opt) => opt.build_command(context),
TEdgeOpt::Run(_) => {
// This method has to be kept in sync with tedge::redirect_if_multicall()
panic!("tedge mapper|agent|write commands are launched as multicall")
}
}
}
}
Expand Down
96 changes: 87 additions & 9 deletions crates/core/tedge/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ use anyhow::Context;
use cap::Cap;
use clap::error::ErrorFormatter;
use clap::error::RichFormatter;
use clap::CommandFactory;
use clap::FromArgMatches;
use clap::Parser;
use std::alloc;
use std::ffi::OsString;
use std::future::Future;
use std::io::IsTerminal;
use std::path::PathBuf;
Expand All @@ -15,8 +18,11 @@ use tedge::command::BuildCommand;
use tedge::command::BuildContext;
use tedge::log::MaybeFancy;
use tedge::Component;
use tedge::ComponentOpt;
use tedge::TEdgeOpt;
use tedge::TEdgeOptMulticall;
use tedge_apt_plugin::AptCli;
use tedge_config::cli::CommonArgs;
use tedge_config::system_services::log_init;
use tracing::log;

Expand All @@ -26,12 +32,7 @@ static ALLOCATOR: Cap<alloc::System> = Cap::new(alloc::System, usize::MAX);
fn main() -> anyhow::Result<()> {
let executable_name = executable_name();

if matches!(executable_name.as_deref(), Some("apt" | "tedge-apt-plugin")) {
let try_opt = AptCli::try_parse();
tedge_apt_plugin::run_and_exit(try_opt);
}

let opt = parse_multicall_if_known(&executable_name);
let opt = parse_multicall(&executable_name, std::env::args_os());
match opt {
TEdgeOptMulticall::Component(Component::TedgeMapper(opt)) => {
let tedge_config = tedge_config::TEdgeConfig::load(&opt.common.config_dir)?;
Expand All @@ -58,6 +59,9 @@ fn main() -> anyhow::Result<()> {
block_on(tedge_watchdog::run(opt))
}
TEdgeOptMulticall::Component(Component::TedgeWrite(opt)) => tedge_write::bin::run(opt),
TEdgeOptMulticall::Component(Component::TedgeAptPlugin(opt)) => {
tedge_apt_plugin::run_and_exit(opt)
}
TEdgeOptMulticall::Tedge { cmd, common } => {
let tedge_config_location =
tedge_config::TEdgeConfigLocation::from_custom_root(&common.config_dir);
Expand Down Expand Up @@ -120,20 +124,94 @@ fn executable_name() -> Option<String> {
)
}

fn parse_multicall_if_known<T: Parser>(executable_name: &Option<String>) -> T {
let cmd = T::command();
fn parse_multicall<Arg, Args>(executable_name: &Option<String>, args: Args) -> TEdgeOptMulticall
where
Args: IntoIterator<Item = Arg>,
Arg: Into<OsString> + Clone,
{
if matches!(executable_name.as_deref(), Some("apt" | "tedge-apt-plugin")) {
// the apt plugin must be treated apart
// as we want to exit 1 and not 2 when the command line cannot be parsed
match AptCli::try_parse() {
Ok(apt) => return TEdgeOptMulticall::Component(Component::TedgeAptPlugin(apt)),
Err(e) => {
eprintln!("{}", RichFormatter::format_error(&e));
std::process::exit(1);
}
}
}

let cmd = TEdgeOptMulticall::command();

let is_known_subcommand = executable_name
.as_deref()
.map_or(false, |name| cmd.find_subcommand(name).is_some());
let cmd = cmd.multicall(is_known_subcommand);

let cmd2 = cmd.clone();
match T::from_arg_matches(&cmd.get_matches()) {
match TEdgeOptMulticall::from_arg_matches(&cmd.get_matches_from(args)) {
Ok(TEdgeOptMulticall::Tedge { cmd, common }) => redirect_if_multicall(cmd, common),
Ok(t) => t,
Err(e) => {
eprintln!("{}", RichFormatter::format_error(&e.with_cmd(&cmd2)));
std::process::exit(1);
}
}
}

// Transform `tedge mapper|agent|write` commands into multicalls
//
// This method has to be kept in sync with TEdgeOpt::build_command
fn redirect_if_multicall(cmd: TEdgeOpt, common: CommonArgs) -> TEdgeOptMulticall {
match cmd {
TEdgeOpt::Run(ComponentOpt { component }) => TEdgeOptMulticall::Component(component),
cmd => TEdgeOptMulticall::Tedge { cmd, common },
}
}

#[cfg(test)]
mod tests {
use crate::parse_multicall;
use crate::Component;
use crate::TEdgeOptMulticall;
use test_case::test_case;

#[test]
fn launching_a_mapper() {
let exec = Some("tedge-mapper".to_string());
let cmd = parse_multicall(&exec, ["tedge-mapper", "c8y"]);
assert!(matches!(
cmd,
TEdgeOptMulticall::Component(Component::TedgeMapper(_))
))
}

#[test]
fn using_tedge_to_launch_a_mapper() {
let exec = Some("tedge".to_string());
let cmd = parse_multicall(&exec, ["tedge", "run", "tedge-mapper", "c8y"]);
assert!(matches!(
cmd,
TEdgeOptMulticall::Component(Component::TedgeMapper(_))
))
}

#[test_case("tedge-mapper c8y --config-dir /some/dir")]
#[test_case("tedge-mapper --config-dir /some/dir c8y")]
#[test_case("tedge run tedge-mapper c8y --config-dir /some/dir")]
#[test_case("tedge run tedge-mapper --config-dir /some/dir c8y")]
#[test_case("tedge --config-dir /some/dir run tedge-mapper c8y")]
// clap fails to raise an error here and takes the inner value for all global args
#[test_case("tedge --config-dir /oops run tedge-mapper c8y --config-dir /some/dir")]
fn setting_config_dir(cmd_line: &'static str) {
let args: Vec<&str> = cmd_line.split(' ').collect();
let exec = Some(args.get(0).unwrap().to_string());
let cmd = parse_multicall(&exec, args);
match cmd {
TEdgeOptMulticall::Component(Component::TedgeMapper(mapper)) => {
assert_eq!(mapper.common.config_dir, "/some/dir")
}
_ => panic!(),
}
}
}
38 changes: 28 additions & 10 deletions docs/src/references/cli/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,11 @@ sidebar_position: 4
# The tedge command

```sh title="tedge"
tedge is the cli tool for thin-edge.io
Command line interface to interact with thin-edge.io

USAGE:
tedge [OPTIONS] [SUBCOMMAND]
Usage: tedge [OPTIONS] <COMMAND>

OPTIONS:
--config-dir <CONFIG_DIR> [env: TEDGE_CONFIG_DIR, default: /etc/tedge]
-h, --help Print help information
--init Initialize the tedge
-V, --version Print version information

SUBCOMMANDS:
Commands:
init Initialize Thin Edge
cert Create and manage device certificate
config Configure Thin Edge
Expand All @@ -28,5 +21,30 @@ SUBCOMMANDS:
refresh-bridges Refresh all currently active mosquitto bridges
upload Upload files to the cloud
mqtt Publish a message on a topic and subscribe a topic
run Run thin-edge services and plugins
help Print this message or the help of the given subcommand(s)

Options:
--config-dir <CONFIG_DIR>
[env: TEDGE_CONFIG_DIR, default: /etc/tedge]

--debug
Turn-on the DEBUG log level.

If off only reports ERROR, WARN, and INFO, if on also reports DEBUG

--log-level <LOG_LEVEL>
Configures the logging level.

One of error/warn/info/debug/trace.
Logs with verbosity lower or equal to the selected level will be printed,
i.e. warn prints ERROR and WARN logs and trace prints logs of all levels.

Overrides `--debug`

-h, --help
Print help (see a summary with '-h')

-V, --version
Print version
```
11 changes: 1 addition & 10 deletions plugins/tedge_apt_plugin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -346,16 +346,7 @@ fn get_config(config_dir: &Path) -> Option<TEdgeConfig> {
}
}

pub fn run_and_exit(cli: Result<AptCli, clap::Error>) -> ! {
let mut apt = match cli {
Ok(aptcli) => aptcli,
Err(err) => {
err.print().expect("Failed to print help message");
// re-write the clap exit_status from 2 to 1, if parse fails
std::process::exit(1)
}
};

pub fn run_and_exit(mut apt: AptCli) -> ! {
if let PluginOp::List { name, maintainer } = &mut apt.operation {
if let Some(config) = get_config(apt.common.config_dir.as_std_path()) {
if name.is_none() {
Expand Down

0 comments on commit bd06fcb

Please sign in to comment.