use std::collections::HashMap;
use std::path::PathBuf;
use anyhow::Result;
use crate::config::LoadedConfig;
use crate::display::Renderer;
use crate::{connect, docker, security};
pub fn run(
cfg: &LoadedConfig,
renderer: &Renderer,
name: &str,
verbose: bool,
user_overrides: &HashMap<String, String>,
) -> Result<()> {
let mut conn = cfg.find_with_wildcard(name)?;
let (expanded_user, user_warnings) = cfg.expand_user_field(&conn.user, user_overrides);
for w in &user_warnings {
renderer.warn(w);
}
conn.user = expanded_user;
if matches!(conn.auth, crate::config::Auth::Identity { .. }) {
renderer.warn(
"this connection is configured as identity-only (e.g. for git hosts) \
and may not support interactive SSH sessions",
);
}
if let Some(key) = conn.auth.key() {
let expanded = expand_tilde(key);
for w in security::check_key_file(&expanded) {
renderer.warn(&w.message);
}
}
if let Some(ref docker_cfg) = cfg.docker {
if !docker::in_container() {
let original_argv: Vec<String> = std::env::args().collect();
docker::exec(docker_cfg, &original_argv, verbose, renderer)?;
unreachable!("docker::exec replaced the process");
}
}
let ssh_args = connect::build_args(&conn);
renderer.print_connecting(&ssh_args);
if verbose {
renderer.verbose_ssh_cmd(&ssh_args);
}
connect::exec(&conn)
}
fn expand_tilde(path: &str) -> PathBuf {
if path == "~" {
return dirs::home_dir().unwrap_or_else(|| PathBuf::from("~"));
}
if let Some(rest) = path.strip_prefix("~/") {
if let Some(home) = dirs::home_dir() {
return home.join(rest);
}
}
PathBuf::from(path)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
use crate::config;
use crate::display::Renderer;
#[derive(Debug)]
enum ConnectPlan {
Docker(Vec<String>),
Ssh(Vec<String>),
}
fn plan(
cfg: &LoadedConfig,
name: &str,
original_argv: &[String],
in_container: bool,
) -> Result<ConnectPlan> {
plan_with_overrides(cfg, name, original_argv, in_container, &HashMap::new())
}
fn plan_with_overrides(
cfg: &LoadedConfig,
name: &str,
original_argv: &[String],
in_container: bool,
user_overrides: &HashMap<String, String>,
) -> Result<ConnectPlan> {
let mut conn = cfg.find_with_wildcard(name)?;
let (expanded_user, _warnings) = cfg.expand_user_field(&conn.user, user_overrides);
conn.user = expanded_user;
if let Some(ref docker_cfg) = cfg.docker {
if !in_container {
let args = docker::build_args(docker_cfg, original_argv)?;
return Ok(ConnectPlan::Docker(args));
}
}
Ok(ConnectPlan::Ssh(connect::build_args(&conn)))
}
fn write_yaml(dir: &std::path::Path, name: &str, content: &str) {
fs::write(dir.join(name), content).unwrap();
}
fn no_color() -> Renderer {
Renderer::new(false)
}
fn load(
cwd: &std::path::Path,
user: Option<&std::path::Path>,
sys: &std::path::Path,
) -> config::LoadedConfig {
config::load_impl(cwd, Some("connections"), false, user, sys).unwrap()
}
fn argv(args: &[&str]) -> Vec<String> {
args.iter().map(|s| s.to_string()).collect()
}
#[test]
fn test_connect_unknown_name_returns_error() {
let cwd = TempDir::new().unwrap();
let empty = TempDir::new().unwrap();
let cfg = load(cwd.path(), None, empty.path());
let err = plan(
&cfg,
"does-not-exist",
&argv(&["yconn", "connect", "does-not-exist"]),
false,
)
.unwrap_err();
assert!(err.to_string().contains("does-not-exist"));
}
#[test]
fn test_connect_error_message_contains_name() {
let cwd = TempDir::new().unwrap();
let empty = TempDir::new().unwrap();
let cfg = load(cwd.path(), None, empty.path());
let err = plan(
&cfg,
"my-server",
&argv(&["yconn", "connect", "my-server"]),
false,
)
.unwrap_err();
assert!(err.to_string().contains("my-server"));
}
#[test]
fn test_connect_no_docker_produces_ssh_plan() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
"connections:\n prod:\n host: 10.0.0.1\n user: deploy\n auth:\n type: key\n key: ~/.ssh/id_rsa\n description: Prod\n",
);
let empty = TempDir::new().unwrap();
let cfg = load(cwd.path(), Some(user.path()), empty.path());
let p = plan(&cfg, "prod", &argv(&["yconn", "connect", "prod"]), false).unwrap();
assert!(matches!(p, ConnectPlan::Ssh(_)));
if let ConnectPlan::Ssh(args) = p {
assert_eq!(args[0], "ssh");
assert!(args.contains(&"-i".to_string()));
assert!(args.contains(&"~/.ssh/id_rsa".to_string()));
assert!(args.last().unwrap().contains("deploy@10.0.0.1"));
}
}
#[test]
fn test_connect_key_auth_default_port_ssh_args() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
"connections:\n srv:\n host: myhost\n user: admin\n auth:\n type: key\n key: ~/.ssh/id_ed25519\n description: Server\n",
);
let empty = TempDir::new().unwrap();
let cfg = load(cwd.path(), Some(user.path()), empty.path());
let p = plan(&cfg, "srv", &argv(&["yconn", "connect", "srv"]), false).unwrap();
if let ConnectPlan::Ssh(args) = p {
assert_eq!(
args,
vec![
"ssh",
"-F",
"/dev/null",
"-i",
"~/.ssh/id_ed25519",
"admin@myhost"
]
);
}
}
#[test]
fn test_connect_password_auth_ssh_args() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
"connections:\n db:\n host: db.internal\n user: dbadmin\n auth:\n type: password\n description: DB\n",
);
let empty = TempDir::new().unwrap();
let cfg = load(cwd.path(), Some(user.path()), empty.path());
let p = plan(&cfg, "db", &argv(&["yconn", "connect", "db"]), false).unwrap();
if let ConnectPlan::Ssh(args) = p {
assert_eq!(args, vec!["ssh", "-F", "/dev/null", "dbadmin@db.internal"]);
}
}
#[test]
fn test_connect_docker_not_in_container_produces_docker_plan() {
let root = TempDir::new().unwrap();
let yconn = root.path().join(".yconn");
fs::create_dir_all(&yconn).unwrap();
write_yaml(
&yconn,
"connections.yaml",
"docker:\n image: ghcr.io/org/keys:latest\nconnections:\n prod:\n host: 10.0.0.1\n user: deploy\n auth:\n type: key\n key: ~/.ssh/id_rsa\n description: Prod\n",
);
let empty = TempDir::new().unwrap();
let cfg = load(root.path(), None, empty.path());
assert!(cfg.docker.is_some());
let p = plan(&cfg, "prod", &argv(&["yconn", "connect", "prod"]), false).unwrap();
assert!(matches!(p, ConnectPlan::Docker(_)));
if let ConnectPlan::Docker(args) = p {
assert_eq!(args[0], "docker");
assert_eq!(args[1], "run");
assert!(args.contains(&"ghcr.io/org/keys:latest".to_string()));
assert!(args.contains(&"CONN_IN_DOCKER=1".to_string()));
}
}
#[test]
fn test_connect_docker_in_container_produces_ssh_plan() {
let root = TempDir::new().unwrap();
let yconn = root.path().join(".yconn");
fs::create_dir_all(&yconn).unwrap();
write_yaml(
&yconn,
"connections.yaml",
"docker:\n image: ghcr.io/org/keys:latest\nconnections:\n prod:\n host: 10.0.0.1\n user: deploy\n auth:\n type: key\n key: ~/.ssh/id_rsa\n description: Prod\n",
);
let empty = TempDir::new().unwrap();
let cfg = load(root.path(), None, empty.path());
let p = plan(&cfg, "prod", &argv(&["yconn", "connect", "prod"]), true).unwrap();
assert!(matches!(p, ConnectPlan::Ssh(_)));
}
#[test]
fn test_connect_docker_argv_passed_through() {
let root = TempDir::new().unwrap();
let yconn = root.path().join(".yconn");
fs::create_dir_all(&yconn).unwrap();
write_yaml(
&yconn,
"connections.yaml",
"docker:\n image: myimage:v1\nconnections:\n srv:\n host: h\n user: u\n auth:\n type: key\n key: ~/.ssh/k\n description: d\n",
);
let empty = TempDir::new().unwrap();
let cfg = load(root.path(), None, empty.path());
let orig = argv(&["yconn", "connect", "srv"]);
let p = plan(&cfg, "srv", &orig, false).unwrap();
if let ConnectPlan::Docker(args) = p {
let img_pos = args.iter().position(|a| a == "myimage:v1").unwrap();
assert_eq!(&args[img_pos + 1..], &["yconn", "connect", "srv"]);
}
}
#[test]
fn test_connect_no_docker_block_goes_ssh() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
"connections:\n srv:\n host: h\n user: u\n auth:\n type: password\n description: d\n",
);
let empty = TempDir::new().unwrap();
let cfg = load(cwd.path(), Some(user.path()), empty.path());
assert!(cfg.docker.is_none());
let p = plan(&cfg, "srv", &argv(&["yconn", "connect", "srv"]), false).unwrap();
assert!(matches!(p, ConnectPlan::Ssh(_)));
}
#[test]
fn test_run_unknown_name_returns_error() {
let cwd = TempDir::new().unwrap();
let empty = TempDir::new().unwrap();
let cfg = load(cwd.path(), None, empty.path());
let err = run(&cfg, &no_color(), "no-such-server", false, &HashMap::new()).unwrap_err();
assert!(err.to_string().contains("no-such-server"));
}
#[test]
fn test_print_connecting_key_auth_format() {
use crate::config::{Auth, Connection, Layer};
use std::path::PathBuf;
let conn = Connection {
name: "srv".to_string(),
host: "myhost".to_string(),
user: "deploy".to_string(),
port: 22,
auth: Auth::Key {
key: "~/.ssh/id_rsa".to_string(),
cmd: None,
},
description: String::new(),
link: None,
group: None,
layer: Layer::User,
source_path: PathBuf::from("test.yaml"),
shadowed: false,
};
let args = crate::connect::build_args(&conn);
let line = format!("[yconn] Connecting: {}", args.join(" "));
assert!(
line.starts_with("[yconn] Connecting: ssh"),
"line must start with '[yconn] Connecting: ssh': {line}"
);
assert!(line.contains("-i"), "line must contain -i flag: {line}");
assert!(
line.contains("~/.ssh/id_rsa"),
"line must contain key path: {line}"
);
assert!(
line.contains("deploy@myhost"),
"line must contain destination: {line}"
);
assert!(
!line.contains('\n'),
"connecting line must be a single line: {line}"
);
}
#[test]
fn test_print_connecting_password_auth_format() {
use crate::config::{Auth, Connection, Layer};
use std::path::PathBuf;
let conn = Connection {
name: "db".to_string(),
host: "db.internal".to_string(),
user: "dbadmin".to_string(),
port: 22,
auth: Auth::Password,
description: String::new(),
link: None,
group: None,
layer: Layer::User,
source_path: PathBuf::from("test.yaml"),
shadowed: false,
};
let args = crate::connect::build_args(&conn);
let line = format!("[yconn] Connecting: {}", args.join(" "));
assert!(
line.starts_with("[yconn] Connecting: ssh"),
"line must start with '[yconn] Connecting: ssh': {line}"
);
assert!(
!line.contains("-i"),
"line must not contain -i for password auth: {line}"
);
assert!(
line.contains("dbadmin@db.internal"),
"line must contain destination: {line}"
);
assert!(
!line.contains('\n'),
"connecting line must be a single line: {line}"
);
}
#[test]
fn test_expand_tilde_prefix_joins_home() {
let result = expand_tilde("~/foo/bar");
let home = dirs::home_dir().expect("home dir must be set in test environment");
assert_eq!(result, home.join("foo/bar"));
}
#[test]
fn test_expand_tilde_bare_returns_home() {
let result = expand_tilde("~");
let home = dirs::home_dir().expect("home dir must be set in test environment");
assert_eq!(result, home);
}
#[test]
fn test_expand_tilde_absolute_path_unchanged() {
let result = expand_tilde("/home/user/.ssh/id_rsa");
assert_eq!(result, std::path::PathBuf::from("/home/user/.ssh/id_rsa"));
}
#[test]
fn test_expand_tilde_no_tilde_unchanged() {
let result = expand_tilde("relative/path/key");
assert_eq!(result, std::path::PathBuf::from("relative/path/key"));
}
#[test]
fn test_tilde_key_exists_no_warning() {
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
let key_path = dir.path().join("id_rsa");
fs::write(&key_path, "KEY").unwrap();
fs::set_permissions(&key_path, fs::Permissions::from_mode(0o600)).unwrap();
let expanded = key_path.clone();
let warnings = security::check_key_file(&expanded);
assert!(
warnings.is_empty(),
"unexpected warnings for existing key: {:?}",
warnings
);
}
#[test]
fn test_tilde_key_missing_warns() {
let dir = TempDir::new().unwrap();
let missing = dir.path().join("no_such_key");
let warnings = security::check_key_file(&missing);
assert_eq!(warnings.len(), 1, "expected exactly one warning");
assert!(
warnings[0].message.contains("does not exist"),
"warning message should say 'does not exist': {}",
warnings[0].message
);
}
#[test]
fn test_expand_tilde_then_check_existing_key_no_warning() {
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
let key_path = dir.path().join("id_ed25519");
fs::write(&key_path, "KEY DATA").unwrap();
fs::set_permissions(&key_path, fs::Permissions::from_mode(0o600)).unwrap();
let expanded = expand_tilde(key_path.to_str().unwrap());
assert_eq!(expanded, key_path);
let warnings = security::check_key_file(&expanded);
assert!(
warnings.is_empty(),
"absolute path to existing key must not warn: {:?}",
warnings
);
}
fn conn_with_user(user: &str) -> config::LoadedConfig {
let cwd = TempDir::new().unwrap();
let user_dir = TempDir::new().unwrap();
let yaml = format!(
"connections:\n srv:\n host: myhost\n user: {user}\n auth:\n type: password\n description: d\n"
);
write_yaml(user_dir.path(), "connections.yaml", &yaml);
let empty = TempDir::new().unwrap();
load(cwd.path(), Some(user_dir.path()), empty.path())
}
fn conn_with_user_and_users_map(user: &str, users_yaml: &str) -> config::LoadedConfig {
let cwd = TempDir::new().unwrap();
let user_dir = TempDir::new().unwrap();
let yaml = format!(
"{users_yaml}connections:\n srv:\n host: myhost\n user: {user}\n auth:\n type: password\n description: d\n"
);
write_yaml(user_dir.path(), "connections.yaml", &yaml);
let empty = TempDir::new().unwrap();
load(cwd.path(), Some(user_dir.path()), empty.path())
}
#[test]
fn test_dollar_user_expands_to_env_user() {
let cfg = conn_with_user("${user}");
let mut overrides = HashMap::new();
overrides.insert("user".to_string(), "alice".to_string());
let p = plan_with_overrides(
&cfg,
"srv",
&argv(&["yconn", "connect", "srv"]),
false,
&overrides,
)
.unwrap();
if let ConnectPlan::Ssh(args) = p {
assert!(
args.last().unwrap().starts_with("alice@"),
"expected alice@..., got {:?}",
args
);
}
}
#[test]
fn test_dollar_user_unset_passes_through() {
let cfg = conn_with_user("${user}");
let (result, _warnings) = cfg.expand_user_field("${user}", &HashMap::new());
assert!(!result.is_empty());
}
#[test]
fn test_named_key_expands_from_users_map() {
let cfg = conn_with_user_and_users_map("${testuser}", "users:\n testuser: ops\n");
let p = plan_with_overrides(
&cfg,
"srv",
&argv(&["yconn", "connect", "srv"]),
false,
&HashMap::new(),
)
.unwrap();
if let ConnectPlan::Ssh(args) = p {
assert!(
args.last().unwrap().starts_with("ops@"),
"expected ops@..., got {:?}",
args
);
}
}
#[test]
fn test_user_override_user_key_overrides_dollar_user() {
let cfg = conn_with_user("${user}");
let mut overrides = HashMap::new();
overrides.insert("user".to_string(), "alice".to_string());
let p = plan_with_overrides(
&cfg,
"srv",
&argv(&["yconn", "connect", "srv"]),
false,
&overrides,
)
.unwrap();
if let ConnectPlan::Ssh(args) = p {
assert!(
args.last().unwrap().starts_with("alice@"),
"expected alice@..., got {:?}",
args
);
}
}
#[test]
fn test_user_override_named_key_shadows_config() {
let cfg = conn_with_user_and_users_map("${testuser}", "users:\n testuser: ops\n");
let mut overrides = HashMap::new();
overrides.insert("testuser".to_string(), "alice".to_string());
let p = plan_with_overrides(
&cfg,
"srv",
&argv(&["yconn", "connect", "srv"]),
false,
&overrides,
)
.unwrap();
if let ConnectPlan::Ssh(args) = p {
assert!(
args.last().unwrap().starts_with("alice@"),
"expected alice@..., got {:?}",
args
);
}
}
#[test]
fn test_multiple_user_overrides_all_applied() {
let cfg = conn_with_user_and_users_map(
"${testuser}",
"users:\n testuser: ops\n other: ignored\n",
);
let mut overrides = HashMap::new();
overrides.insert("testuser".to_string(), "carol".to_string());
overrides.insert("other".to_string(), "dave".to_string());
let p = plan_with_overrides(
&cfg,
"srv",
&argv(&["yconn", "connect", "srv"]),
false,
&overrides,
)
.unwrap();
if let ConnectPlan::Ssh(args) = p {
assert!(
args.last().unwrap().starts_with("carol@"),
"expected carol@..., got {:?}",
args
);
}
}
#[test]
fn test_unresolved_template_warns() {
let cfg = conn_with_user("${unknown_key}");
let (_result, warnings) = cfg.expand_user_field("${unknown_key}", &HashMap::new());
assert!(
!warnings.is_empty(),
"expected at least one warning for unresolved template"
);
assert!(
warnings[0].contains("unresolved"),
"warning should mention 'unresolved': {}",
warnings[0]
);
}
}