//! `attach` subcommand for `ec2` use crate::aws::{ deployer_directory, utils::ssh_attach, Error, Hosts, Metadata, CREATED_FILE_NAME, DESTROYED_FILE_NAME, METADATA_FILE_NAME, MONITORING_REGION, }; use std::{ fs::{self, File}, net::IpAddr, }; use tracing::{info, warn}; const HOSTS_FILE_NAME: &str = "hosts.yaml"; #[derive(Clone, Debug, Eq, PartialEq)] struct InstanceMatch { tag: String, region: String, ip: String, } /// Opens an interactive SSH session to a deployed instance. pub async fn attach(ip: &str) -> Result<(), Error> { let ip = normalize_ip(ip)?; for metadata in load_active_deployments()? { if let Some(instance) = find_match(&metadata, &ip) { return attach_to_instance(&instance).await; } } Err(Error::InstanceNotFound(ip)) } async fn attach_to_instance(instance: &InstanceMatch) -> Result<(), Error> { let key_path = deployer_directory(Some(&instance.tag)).join(format!("id_rsa_{}", instance.tag)); if !key_path.exists() { return Err(Error::PrivateKeyNotFound); } info!( tag = instance.tag.as_str(), region = instance.region.as_str(), ip = instance.ip.as_str(), "attaching to instance" ); ssh_attach(key_path.to_str().unwrap(), &instance.ip).await } fn normalize_ip(ip: &str) -> Result { Ok(ip.parse::()?.to_string()) } fn load_active_deployments() -> Result, Error> { let deployer_dir = deployer_directory(None); if !deployer_dir.exists() { return Ok(Vec::new()); } let mut deployments: Vec = Vec::new(); for entry in fs::read_dir(&deployer_dir)? { let path = entry?.path(); if !path.is_dir() { continue; } if path.join(DESTROYED_FILE_NAME).exists() { continue; } if !path.join(CREATED_FILE_NAME).exists() { continue; } let metadata_path = path.join(METADATA_FILE_NAME); let Ok(file) = File::open(&metadata_path) else { continue; }; let Ok(metadata) = serde_yaml::from_reader(file) else { continue; }; deployments.push(metadata); } deployments.sort_by(|a, b| a.tag.cmp(&b.tag)); Ok(deployments) } fn find_match(metadata: &Metadata, ip: &str) -> Option { let hosts_path = deployer_directory(Some(&metadata.tag)).join(HOSTS_FILE_NAME); let file = match File::open(&hosts_path) { Ok(file) => file, Err(error) => { warn!( tag = metadata.tag.as_str(), path = ?hosts_path, ?error, "failed to open hosts file while looking for attach target" ); return None; } }; let hosts = match serde_yaml::from_reader::<_, Hosts>(file) { Ok(hosts) => hosts, Err(error) => { warn!( tag = metadata.tag.as_str(), path = ?hosts_path, ?error, "failed to parse hosts file while looking for attach target" ); return None; } }; find_host_match(&metadata.tag, hosts, ip) } fn find_host_match(tag: &str, hosts: Hosts, ip: &str) -> Option { if hosts.monitoring.public.to_string() == ip { return Some(InstanceMatch { tag: tag.to_string(), region: MONITORING_REGION.to_string(), ip: ip.to_string(), }); } hosts .hosts .into_iter() .find(|host| host.ip.to_string() == ip) .map(|host| InstanceMatch { tag: tag.to_string(), region: host.region, ip: ip.to_string(), }) } #[cfg(test)] mod tests { use super::{find_host_match, normalize_ip, InstanceMatch}; use crate::aws::{Host, Hosts, Ips, MONITORING_REGION}; use std::net::IpAddr; fn instance(tag: &str, region: &str, ip: &str) -> InstanceMatch { InstanceMatch { tag: tag.to_string(), region: region.to_string(), ip: ip.to_string(), } } #[test] fn test_normalize_ip() { assert_eq!( normalize_ip("2001:db8:0:0::1").expect("failed to parse ip"), "2001:db8::1" ); } #[test] fn test_instance_match() { let selected = instance("a", "us-east-1", "1.1.1.1"); assert_eq!(selected.tag, "a"); } #[test] fn test_find_host_match() { let hosts = Hosts { monitoring: Ips { public: "2.2.2.2".parse::().unwrap(), private: "10.0.0.1".parse::().unwrap(), }, hosts: vec![Host { name: "node".to_string(), region: "us-east-1".to_string(), ip: "1.1.1.1".parse::().unwrap(), }], }; let selected = find_host_match("tag", hosts, "1.1.1.1").expect("match missing"); assert_eq!(selected.tag, "tag"); assert_eq!(selected.region, "us-east-1"); } #[test] fn test_find_monitoring_match() { let hosts = Hosts { monitoring: Ips { public: "2.2.2.2".parse::().unwrap(), private: "10.0.0.1".parse::().unwrap(), }, hosts: Vec::new(), }; let selected = find_host_match("tag", hosts, "2.2.2.2").expect("match missing"); assert_eq!(selected.tag, "tag"); assert_eq!(selected.region, MONITORING_REGION); } }