homestar_runtime/
cli.rs

1//! CLI commands/arguments.
2
3use crate::{
4    network::rpc::Client,
5    runner::{file, response},
6    KeyType,
7};
8use anyhow::anyhow;
9use clap::{ArgGroup, Args, Parser, Subcommand};
10use serde::{Deserialize, Serialize};
11use std::{
12    net::{IpAddr, Ipv6Addr, SocketAddr},
13    path::PathBuf,
14    time::{Duration, SystemTime},
15};
16use tarpc::context;
17
18mod error;
19pub use error::Error;
20mod init;
21pub use init::{handle_init_command, KeyArg, OutputMode};
22pub(crate) mod show;
23pub use show::ConsoleTable;
24
25const DEFAULT_DB_PATH: &str = "homestar.db";
26const TMP_DIR: &str = "/tmp";
27const HELP_TEMPLATE: &str = "{name} {version}
28
29{about}
30
31Usage: {usage}
32
33{all-args}
34";
35
36/// CLI arguments.
37#[derive(Debug, Parser)]
38#[command(bin_name = "homestar", name = "homestar", author, version, about,
39          long_about = None, help_template = HELP_TEMPLATE)]
40#[clap(group(ArgGroup::new("init_sink").args(&["config", "dry-run"])))]
41#[clap(group(ArgGroup::new("init_key_arg").args(&["key-file", "key-seed"])))]
42pub struct Cli {
43    /// Homestar [Command].
44    #[clap(subcommand)]
45    pub command: Command,
46}
47
48/// Arguments for `init` command.
49#[derive(Debug, Clone, PartialEq, Args)]
50pub struct InitArgs {
51    /// Runtime configuration file (.toml).
52    #[arg(
53            short = 'o',
54            long = "output",
55            value_hint = clap::ValueHint::FilePath,
56            value_name = "OUTPUT",
57            help = "Path to write initialized configuration file (.toml) [optional]",
58            group = "init_sink"
59        )]
60    pub output_path: Option<PathBuf>,
61    /// Skip writing to disk.
62    #[arg(
63        long = "dry-run",
64        help = "Skip writing to disk",
65        default_value = "false",
66        help = "Skip writing to disk, instead writing configuration to stdout [optional]",
67        group = "init_sink"
68    )]
69    pub dry_run: bool,
70    /// Suppress auxiliary output.
71    #[arg(
72        short = 'q',
73        long = "quiet",
74        default_value = "false",
75        help = "Suppress auxiliary output [optional]"
76    )]
77    pub quiet: bool,
78    /// Force destructive operations without prompting.
79    #[arg(
80        short = 'f',
81        long = "force",
82        default_value = "false",
83        help = "Force destructive operations without prompting [optional]"
84    )]
85    pub force: bool,
86    /// Run in non-interactive mode by disabling all prompts.
87    #[arg(
88        long = "no-input",
89        default_value = "false",
90        help = "Run in non-interactive mode [optional]"
91    )]
92    pub no_input: bool,
93    /// The type of key to use for libp2p
94    #[arg(
95        long = "key-type",
96        value_name = "KEY_TYPE",
97        help = "The type of key to use for libp2p [optional]"
98    )]
99    pub key_type: Option<KeyType>,
100    /// The file to load the key from
101    #[arg(
102        long = "key-file",
103        value_name = "KEY_FILE",
104        help = "The path to the key file. A key will be generated if the file does not exist, and if left unspecified, a default path will be used. [optional]",
105        group = "init_key_arg"
106    )]
107    pub key_file: Option<Option<PathBuf>>,
108    /// The seed to use for generating the key
109    #[arg(
110        long = "key-seed",
111        value_name = "KEY_SEED",
112        help = "The seed to use for generating the key. If left unspecified, a random seed will be chosen [optional]",
113        group = "init_key_arg"
114    )]
115    pub key_seed: Option<Option<String>>,
116}
117
118/// General RPC arguments for [Client] commands.
119///
120/// [Client]: crate::network::rpc::Client
121#[derive(Debug, Clone, PartialEq, Args, Serialize, Deserialize)]
122pub struct RpcArgs {
123    /// Homestar RPC host.
124    #[clap(
125            long = "host",
126            default_value = "::1",
127            value_hint = clap::ValueHint::Hostname
128        )]
129    host: IpAddr,
130    /// Homestar RPC port.
131    #[clap(short = 'p', long = "port", default_value_t = 3030)]
132    port: u16,
133    /// Homestar RPC timeout.
134    #[clap(long = "timeout", default_value = "60s", value_parser = humantime::parse_duration)]
135    timeout: Duration,
136}
137
138impl Default for RpcArgs {
139    fn default() -> Self {
140        Self {
141            host: Ipv6Addr::LOCALHOST.into(),
142            port: 3030,
143            timeout: Duration::from_secs(60),
144        }
145    }
146}
147
148/// CLI Argument types.
149#[derive(Debug, Subcommand)]
150pub enum Command {
151    /// Initialize a Homestar configuration.
152    Init(InitArgs),
153    /// Start the Homestar runtime.
154    Start {
155        /// Database URL, defaults to homestar.db.
156        #[arg(
157            long = "db",
158            value_name = "DB",
159            env = "DATABASE_PATH",
160            value_hint = clap::ValueHint::AnyPath,
161            value_name = "DATABASE_PATH",
162            default_value = DEFAULT_DB_PATH,
163            help = "Database path (SQLite) [optional]"
164        )]
165        database_url: Option<String>,
166        /// Runtime configuration file (.toml).
167        #[arg(
168            short = 'c',
169            long = "config",
170            value_hint = clap::ValueHint::FilePath,
171            value_name = "CONFIG",
172            help = "Runtime configuration file (.toml) [optional]"
173        )]
174        runtime_config: Option<PathBuf>,
175        /// Daemonize the runtime, false by default.
176        #[arg(
177            short = 'd',
178            long = "daemonize",
179            default_value = "false",
180            help = "Daemonize the runtime"
181        )]
182        daemonize: bool,
183        /// Directory to place daemon files, defaults to /tmp.
184        #[arg(
185            long = "daemon_dir",
186            default_value = TMP_DIR,
187            value_hint = clap::ValueHint::DirPath,
188            value_name = "DIR",
189            help = "Directory to place daemon file(s)"
190        )]
191        daemon_dir: PathBuf,
192    },
193    /// Stop the Homestar runtime.
194    Stop(RpcArgs),
195    /// Ping the Homestar runtime to see if it's running.
196    Ping(RpcArgs),
197    /// Run an IPVM-configured workflow file on the Homestar runtime.
198    Run {
199        /// RPC host / port arguments.
200        #[clap(flatten)]
201        args: RpcArgs,
202        /// Local name associated with a workflow (optional).
203        #[arg(
204            short = 'n',
205            long = "name",
206            value_name = "NAME",
207            help = "Local name given to a workflow (optional)"
208        )]
209        name: Option<String>,
210        /// IPVM-configured workflow file to run.
211        /// Supported:
212        ///   - JSON (.json).
213        #[arg(
214            value_hint = clap::ValueHint::FilePath,
215            value_name = "FILE",
216            value_parser = clap::value_parser!(file::ReadWorkflow),
217            index = 1,
218            required = true,
219            help = r#"IPVM-configured workflow file to run.
220Supported:
221  - JSON (.json)"#
222        )]
223        workflow: file::ReadWorkflow,
224    },
225    /// Get node identity / information.
226    Node {
227        /// RPC host / port arguments.
228        #[clap(flatten)]
229        args: RpcArgs,
230    },
231    /// Get Homestar binary and other information.
232    Info,
233}
234
235impl Command {
236    fn name(&self) -> &'static str {
237        match self {
238            Command::Init { .. } => "init",
239            Command::Start { .. } => "start",
240            Command::Stop { .. } => "stop",
241            Command::Ping { .. } => "ping",
242            Command::Run { .. } => "run",
243            Command::Node { .. } => "node",
244            Command::Info => "info",
245        }
246    }
247
248    /// Handle CLI commands related to [Client] RPC calls.
249    pub fn handle_rpc_command(self) -> Result<(), Error> {
250        // Spin up a new tokio runtime on the current thread.
251        let rt = tokio::runtime::Builder::new_current_thread()
252            .enable_all()
253            .build()?;
254
255        match self {
256            Command::Ping(args) => {
257                let (client, response) = rt.block_on(async {
258                    let client = args.client().await?;
259                    let response = client.ping().await?;
260                    Ok::<(Client, String), Error>((client, response))
261                })?;
262
263                let response = response::Ping::new(client.addr(), response);
264                response.echo_table()?;
265                Ok(())
266            }
267            Command::Stop(args) => rt.block_on(async {
268                let client = args.client().await?;
269                client.stop().await??;
270                Ok(())
271            }),
272            Command::Run {
273                args,
274                name,
275                workflow: workflow_file,
276            } => {
277                let response = rt.block_on(async {
278                    let client = args.client().await?;
279                    let response = client.run(name.map(|n| n.into()), workflow_file).await??;
280                    Ok::<Box<response::AckWorkflow>, Error>(response)
281                })?;
282
283                response.echo_table()?;
284                Ok(())
285            }
286            Command::Node { args } => {
287                let response = rt.block_on(async {
288                    let client = args.client().await?;
289                    let response = client.node_info().await??;
290                    Ok::<response::AckNodeInfo, Error>(response)
291                })?;
292
293                response.echo_table()?;
294                Ok(())
295            }
296            _ => Err(anyhow!("Invalid command {}", self.name()).into()),
297        }
298    }
299}
300
301impl RpcArgs {
302    async fn client(&self) -> Result<Client, Error> {
303        let addr = SocketAddr::new(self.host, self.port);
304        let mut ctx = context::current();
305        ctx.deadline = SystemTime::now() + self.timeout;
306        let client = Client::new(addr, ctx).await?;
307        Ok(client)
308    }
309}