agave_validator/commands/plugin/
mod.rs

1use {
2    crate::{
3        admin_rpc_service,
4        commands::{FromClapArgMatches, Result},
5    },
6    clap::{value_t, App, AppSettings, Arg, ArgMatches, SubCommand},
7    std::path::Path,
8};
9
10const COMMAND: &str = "plugin";
11
12#[derive(Debug, PartialEq)]
13pub struct PluginUnloadArgs {
14    pub name: String,
15}
16
17impl FromClapArgMatches for PluginUnloadArgs {
18    fn from_clap_arg_match(matches: &ArgMatches) -> Result<Self> {
19        Ok(PluginUnloadArgs {
20            name: value_t!(matches, "name", String)?,
21        })
22    }
23}
24
25#[derive(Debug, PartialEq)]
26pub struct PluginLoadArgs {
27    pub config: String,
28}
29
30impl FromClapArgMatches for PluginLoadArgs {
31    fn from_clap_arg_match(matches: &ArgMatches) -> Result<Self> {
32        Ok(PluginLoadArgs {
33            config: value_t!(matches, "config", String)?,
34        })
35    }
36}
37
38#[derive(Debug, PartialEq)]
39pub struct PluginReloadArgs {
40    pub name: String,
41    pub config: String,
42}
43
44impl FromClapArgMatches for PluginReloadArgs {
45    fn from_clap_arg_match(matches: &ArgMatches) -> Result<Self> {
46        Ok(PluginReloadArgs {
47            name: value_t!(matches, "name", String)?,
48            config: value_t!(matches, "config", String)?,
49        })
50    }
51}
52
53pub fn command<'a>() -> App<'a, 'a> {
54    let name_arg = Arg::with_name("name").required(true).takes_value(true);
55    let config_arg = Arg::with_name("config").required(true).takes_value(true);
56
57    SubCommand::with_name(COMMAND)
58        .about("Manage and view geyser plugins")
59        .setting(AppSettings::SubcommandRequiredElseHelp)
60        .setting(AppSettings::InferSubcommands)
61        .subcommand(SubCommand::with_name("list").about("List all current running geyser plugins"))
62        .subcommand(
63            SubCommand::with_name("unload")
64                .about("Unload a particular geyser plugin. You must specify the geyser plugin name")
65                .arg(&name_arg),
66        )
67        .subcommand(
68            SubCommand::with_name("reload")
69                .about(
70                    "Reload a particular geyser plugin. You must specify the geyser plugin name \
71                     and the new config path",
72                )
73                .arg(&name_arg)
74                .arg(&config_arg),
75        )
76        .subcommand(
77            SubCommand::with_name("load")
78                .about(
79                    "Load a new geyser plugin. You must specify the config path. Fails if \
80                     overwriting (use reload)",
81                )
82                .arg(&config_arg),
83        )
84}
85
86pub fn execute(matches: &ArgMatches, ledger_path: &Path) -> Result<()> {
87    match matches.subcommand() {
88        ("list", _) => {
89            let admin_client = admin_rpc_service::connect(ledger_path);
90            let plugins = admin_rpc_service::runtime()
91                .block_on(async move { admin_client.await?.list_plugins().await })?;
92            if !plugins.is_empty() {
93                println!("Currently the following plugins are loaded:");
94                for (plugin, i) in plugins.into_iter().zip(1..) {
95                    println!("  {i}) {plugin}");
96                }
97            } else {
98                println!("There are currently no plugins loaded");
99            }
100        }
101        ("unload", Some(subcommand_matches)) => {
102            let PluginUnloadArgs { name } =
103                PluginUnloadArgs::from_clap_arg_match(subcommand_matches)?;
104
105            let admin_client = admin_rpc_service::connect(ledger_path);
106            admin_rpc_service::runtime()
107                .block_on(async { admin_client.await?.unload_plugin(name.clone()).await })?;
108            println!("Successfully unloaded plugin: {name}");
109        }
110        ("load", Some(subcommand_matches)) => {
111            let PluginLoadArgs { config } =
112                PluginLoadArgs::from_clap_arg_match(subcommand_matches)?;
113
114            let admin_client = admin_rpc_service::connect(ledger_path);
115            let name = admin_rpc_service::runtime()
116                .block_on(async { admin_client.await?.load_plugin(config.clone()).await })?;
117            println!("Successfully loaded plugin: {name}");
118        }
119        ("reload", Some(subcommand_matches)) => {
120            let PluginReloadArgs { name, config } =
121                PluginReloadArgs::from_clap_arg_match(subcommand_matches)?;
122
123            let admin_client = admin_rpc_service::connect(ledger_path);
124            admin_rpc_service::runtime().block_on(async {
125                admin_client
126                    .await?
127                    .reload_plugin(name.clone(), config.clone())
128                    .await
129            })?;
130            println!("Successfully reloaded plugin: {name}");
131        }
132        _ => unreachable!(),
133    }
134
135    Ok(())
136}
137
138#[cfg(test)]
139mod tests {
140    use {super::*, crate::commands::tests::verify_args_struct_by_command_is_error};
141
142    #[test]
143    fn verify_args_struct_by_command_plugin_unload_default() {
144        verify_args_struct_by_command_is_error::<PluginUnloadArgs>(
145            command(),
146            vec![COMMAND, "unload"],
147        );
148    }
149
150    #[test]
151    fn verify_args_struct_by_command_plugin_unload_with_name() {
152        let app = command();
153        let matches = app.get_matches_from(vec![COMMAND, "unload", "testname"]);
154        let subcommand_matches = matches.subcommand_matches("unload").unwrap();
155        let args = PluginUnloadArgs::from_clap_arg_match(subcommand_matches).unwrap();
156        assert_eq!(
157            args,
158            PluginUnloadArgs {
159                name: "testname".to_string(),
160            }
161        );
162    }
163
164    #[test]
165    fn verify_args_struct_by_command_plugin_load_default() {
166        verify_args_struct_by_command_is_error::<PluginLoadArgs>(command(), vec![COMMAND, "load"]);
167    }
168
169    #[test]
170    fn verify_args_struct_by_command_plugin_load_with_config() {
171        let app = command();
172        let matches = app.get_matches_from(vec![COMMAND, "load", "testconfig"]);
173        let subcommand_matches = matches.subcommand_matches("load").unwrap();
174        let args = PluginLoadArgs::from_clap_arg_match(subcommand_matches).unwrap();
175        assert_eq!(
176            args,
177            PluginLoadArgs {
178                config: "testconfig".to_string(),
179            }
180        );
181    }
182
183    #[test]
184    fn verify_args_struct_by_command_plugin_reload_default() {
185        verify_args_struct_by_command_is_error::<PluginReloadArgs>(
186            command(),
187            vec![COMMAND, "reload"],
188        );
189    }
190
191    #[test]
192    fn verify_args_struct_by_command_plugin_reload_with_name() {
193        verify_args_struct_by_command_is_error::<PluginReloadArgs>(
194            command(),
195            vec![COMMAND, "reload", "testname"],
196        );
197    }
198
199    #[test]
200    fn verify_args_struct_by_command_plugin_reload_with_name_and_config() {
201        let app = command();
202        let matches = app.get_matches_from(vec![COMMAND, "reload", "testname", "testconfig"]);
203        let subcommand_matches = matches.subcommand_matches("reload").unwrap();
204        let args = PluginReloadArgs::from_clap_arg_match(subcommand_matches).unwrap();
205        assert_eq!(
206            args,
207            PluginReloadArgs {
208                name: "testname".to_string(),
209                config: "testconfig".to_string(),
210            }
211        );
212    }
213}