/ src / cmdgen.rs
cmdgen.rs
  1  use std::process::Command;
  2  
  3  use crate::instance_details::InstanceDetails;
  4  use crate::{config::Config, opts::ConnectOptions};
  5  
  6  use anyhow::Result;
  7  
  8  pub struct CommandGenerator {
  9      opts: ConnectOptions,
 10      config: Config,
 11      instance: InstanceDetails,
 12  }
 13  
 14  impl CommandGenerator {
 15      pub fn new(opts: &ConnectOptions, config: Config, instance: InstanceDetails) -> Result<Self> {
 16          Ok(Self {
 17              opts: opts.clone(),
 18              config,
 19              instance,
 20          })
 21      }
 22  
 23      pub fn generate<'a>(&self, cmd: &'a mut Command) -> Result<&'a mut Command> {
 24          let ssh_command = vec![
 25              String::from("ssh -t"),
 26              format!("{}@{}", self.user()?, self.address()?),
 27              self.key()?,
 28              self.jumphost()?,
 29          ]
 30          .into_iter()
 31          .filter(|arg| !arg.is_empty())
 32          .collect::<Vec<String>>()
 33          .join(" ");
 34  
 35          Ok(cmd
 36              .arg("-c")
 37              .arg(ssh_command)
 38              .stdin(std::process::Stdio::inherit())
 39              .stdout(std::process::Stdio::inherit()))
 40      }
 41  
 42      fn jumphost(&self) -> Result<String> {
 43          match self.opts.jumphost.clone() {
 44              Some(jumphost) => Ok(format!("-J {}", jumphost)),
 45              None => match self.config.jumphost.clone() {
 46                  Some(jumphost) => match jumphost.as_str() {
 47                      "" => Ok(String::new()),
 48                      jmp => Ok(format!("-J {}", jmp)),
 49                  },
 50                  None => Ok(String::new()),
 51              },
 52          }
 53      }
 54  
 55      fn key(&self) -> Result<String> {
 56          let key = self.opts.key.clone().or(self.config.private_key.clone());
 57  
 58          match key {
 59              Some(key) => Ok(format!("-i {}", shellexpand::tilde(key.to_str().unwrap()))),
 60              None => Ok(String::new()),
 61          }
 62      }
 63  
 64      fn address(&self) -> Result<String> {
 65          match self.opts.address_type.clone().unwrap_or_default().as_str() {
 66              "" => match self.config.address_type.clone() {
 67                  Some(address_type) => match address_type.as_str() {
 68                      "public" => Ok(self.instance.public_ip.clone().unwrap_or_default()),
 69                      "private" => Ok(self.instance.private_ip.clone().unwrap_or_default()),
 70                      _ => Err(anyhow::anyhow!("Invalid address type")),
 71                  },
 72                  None => Ok(self.instance.private_ip.clone().unwrap_or_default()),
 73              },
 74              address_type => match address_type {
 75                  "public" => Ok(self.instance.public_ip.clone().unwrap_or_default()),
 76                  "private" => Ok(self.instance.private_ip.clone().unwrap_or_default()),
 77                  _ => Err(anyhow::anyhow!("Invalid address type")),
 78              },
 79          }
 80      }
 81  
 82      fn user(&self) -> Result<String> {
 83          match self.opts.user.clone().unwrap_or_default().as_str() {
 84              "" => match self.config.default_user.clone() {
 85                  Some(default_user) => Ok(default_user.to_string()),
 86                  None => Err(anyhow::anyhow!("No username provided. Please use --user or configure default username in ~/.config/blssh/config.toml")),
 87              },
 88              username => Ok(username.to_string()),
 89          }
 90      }
 91  }
 92  
 93  #[cfg(test)]
 94  mod tests {
 95      use super::*;
 96  
 97      #[test]
 98      fn jumphost_without_opt_uses_config_opt() {
 99          let config = Config {
100              default_user: None,
101              private_key: None,
102              jumphost: Some(String::from("config-jumphost")),
103              port: None,
104              address_type: None,
105          };
106          let opts = ConnectOptions {
107              user: None,
108              key: None,
109              jumphost: None,
110              address_type: None,
111              port: None,
112              search: None,
113          };
114          let instance = InstanceDetails {
115              instance_id: Some(String::from("id")),
116              instance_name: Some(String::from("name")),
117              public_ip: None,
118              private_ip: None,
119          };
120  
121          let command_generator = CommandGenerator::new(&opts, config, instance).unwrap();
122          assert_eq!(command_generator.jumphost().unwrap(), "-J config-jumphost");
123      }
124  
125      #[test]
126      fn jumphost_with_opt_prioritizes_opt() {
127          let config = Config {
128              default_user: None,
129              private_key: None,
130              jumphost: Some(String::from("config-jumphost")),
131              port: None,
132              address_type: None,
133          };
134          let opts = ConnectOptions {
135              user: None,
136              key: None,
137              jumphost: Some(String::from("opt-jumphost")),
138              address_type: None,
139              port: None,
140              search: None,
141          };
142          let instance = InstanceDetails {
143              instance_id: Some(String::from("id")),
144              instance_name: Some(String::from("name")),
145              public_ip: None,
146              private_ip: None,
147          };
148  
149          let command_generator = CommandGenerator::new(&opts, config, instance).unwrap();
150          assert_eq!(command_generator.jumphost().unwrap(), "-J opt-jumphost");
151      }
152  
153      #[test]
154      fn user_with_opt_prioritizes_opt() {
155          let config = Config {
156              default_user: Some(String::from("default-user")),
157              private_key: None,
158              jumphost: None,
159              port: None,
160              address_type: None,
161          };
162          let opts = ConnectOptions {
163              user: Some(String::from("opt-user")),
164              key: None,
165              jumphost: None,
166              address_type: None,
167              port: None,
168              search: None,
169          };
170          let instance = InstanceDetails {
171              instance_id: Some(String::from("id")),
172              instance_name: Some(String::from("name")),
173              public_ip: None,
174              private_ip: None,
175          };
176  
177          let command_generator = CommandGenerator::new(&opts, config, instance).unwrap();
178          assert_eq!(command_generator.user().unwrap(), "opt-user");
179      }
180  
181      #[test]
182      fn user_without_opt_uses_config_opt() {
183          let config = Config {
184              default_user: Some(String::from("default-user")),
185              private_key: None,
186              jumphost: None,
187              port: None,
188              address_type: None,
189          };
190          let opts = ConnectOptions {
191              user: None,
192              key: None,
193              jumphost: None,
194              address_type: None,
195              port: None,
196              search: None,
197          };
198  
199          let instance = InstanceDetails {
200              instance_id: Some(String::from("id")),
201              instance_name: Some(String::from("name")),
202              public_ip: None,
203              private_ip: None,
204          };
205  
206          let command_generator = CommandGenerator::new(&opts, config, instance).unwrap();
207          assert_eq!(command_generator.user().unwrap(), "default-user");
208      }
209  
210      #[test]
211      fn address_without_opt_uses_config_opt() {
212          let config = Config {
213              default_user: None,
214              private_key: None,
215              jumphost: None,
216              port: None,
217              address_type: Some(String::from("public")),
218          };
219          let opts = ConnectOptions {
220              user: None,
221              key: None,
222              jumphost: None,
223              address_type: None,
224              port: None,
225              search: None,
226          };
227          let instance = InstanceDetails {
228              instance_id: Some(String::from("id")),
229              instance_name: Some(String::from("name")),
230              public_ip: Some(String::from("public-ip")),
231              private_ip: Some(String::from("private-ip")),
232          };
233  
234          let command_generator = CommandGenerator::new(&opts, config, instance).unwrap();
235          assert_eq!(command_generator.address().unwrap(), "public-ip");
236      }
237  
238      #[test]
239      fn address_with_opt_prioritizes_opt() {
240          let config = Config {
241              default_user: None,
242              private_key: None,
243              jumphost: None,
244              port: None,
245              address_type: None,
246          };
247          let opts = ConnectOptions {
248              user: None,
249              key: None,
250              jumphost: None,
251              address_type: Some(String::from("public")),
252              port: None,
253              search: None,
254          };
255          let instance = InstanceDetails {
256              instance_id: Some(String::from("id")),
257              instance_name: Some(String::from("name")),
258              public_ip: Some(String::from("public-ip")),
259              private_ip: Some(String::from("private-ip")),
260          };
261  
262          let command_generator = CommandGenerator::new(&opts, config, instance).unwrap();
263          assert_eq!(command_generator.address().unwrap(), "public-ip");
264      }
265  }