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 }