wasmer_cli/commands/
ssh.rs1use anyhow::Context;
4use wasmer_backend_api::{WasmerClient, types::DeployApp};
5
6use super::{AsyncCliCommand, app::AppIdentArgOpts};
7use crate::{config::WasmerEnv, edge_config::LoadedEdgeConfig};
8
9#[derive(clap::Parser, Debug)]
11pub struct CmdSsh {
12 #[clap(flatten)]
13 pub app: AppIdentArgOpts,
14
15 #[clap(flatten)]
16 env: WasmerEnv,
17
18 #[clap(long, default_value = "22")]
20 pub ssh_port: u16,
21
22 #[clap(long)]
26 pub map_port: Vec<u16>,
27
28 #[clap(long)]
32 pub host: Option<String>,
33
34 #[clap(short, long)]
36 pub print: bool,
37
38 #[clap(trailing_var_arg = true)]
40 pub extra: Vec<String>,
41}
42
43#[async_trait::async_trait]
44impl AsyncCliCommand for CmdSsh {
45 type Output = ();
46
47 async fn run_async(self) -> Result<(), anyhow::Error> {
48 let mut config = crate::edge_config::load_config(None)?;
49 let client = self.env.client()?;
50
51 let token = acquire_ssh_token(&client, &config, &self.app).await?;
52 let app_id = token.app.as_ref().map(|a| a.id.inner().to_owned());
53
54 let host = if let Some(host) = self.host {
55 host
56 } else if let Some(app) = &token.app {
57 let url = app.url.clone();
59 if url.starts_with("http://") || url.starts_with("https://") {
60 let url = url.parse::<url::Url>().context("Could not parse app url")?;
61 url.host_str()
62 .context("Could not determine host from app url")?
63 .to_string()
64 } else {
65 url
66 }
67 } else {
68 self.env
71 .app_domain()
72 .context("Could not determine SSH host based on the backend url")?
73 };
74 let port = self.ssh_port;
75
76 if token.is_new {
77 eprintln!("Acquired new SSH token");
78 config.config.add_ssh_token(app_id, token.token.clone());
79 if let Err(err) = config.save() {
80 eprintln!("WARNING: failed to save config: {err}");
81 }
82 }
83
84 if let Some(app) = &token.app {
85 eprintln!("Connecting to app '{}' at {}", app.name, host);
86 }
87
88 let mut cmd = std::process::Command::new("ssh");
89 let mut cmd = cmd
90 .args([
91 "-o",
93 "ControlPath=none",
94 "-o",
97 "StrictHostKeyChecking=no",
98 "-o",
101 "UserKnownHostsFile=/dev/null",
102 "-o",
103 "IdentitiesOnly=yes",
104 "-q",
106 ])
107 .args(["-p", format!("{port}").as_str()]);
108 for map_port in self.map_port {
109 cmd = cmd.args(["-L", format!("{map_port}:localhost:{map_port}").as_str()]);
110 }
111
112 cmd = cmd
113 .arg(format!("{}@{}", token.token, host))
114 .args(&self.extra);
115
116 if self.print {
117 print!("ssh");
118 for arg in cmd.get_args() {
119 print!(" {}", arg.to_string_lossy().as_ref());
120 }
121 println!();
122 return Ok(());
123 }
124
125 let exit = cmd.spawn()?.wait()?;
126 if exit.success() {
127 Ok(())
128 } else {
129 Err(anyhow::anyhow!("ssh failed with status {exit}"))
130 }
131 }
132}
133
134type RawToken = String;
135
136struct AcquiredToken {
137 token: RawToken,
138 app: Option<DeployApp>,
139 is_new: bool,
140}
141
142async fn acquire_ssh_token(
143 client: &WasmerClient,
144 config: &LoadedEdgeConfig,
145 app_opts: &AppIdentArgOpts,
146) -> Result<AcquiredToken, anyhow::Error> {
147 let app_opts = app_opts.clone().to_opts();
148
149 let app = if let Some((_, app)) = app_opts.load_app_opt(client).await? {
150 Some(app)
151 } else {
152 None
153 };
154
155 let app_id = app.as_ref().map(|a| a.id.inner());
157 if let Some(token) = config.config.get_valid_ssh_token(app_id) {
158 return Ok(AcquiredToken {
159 token: token.to_string(),
160 app,
161 is_new: false,
162 });
163 }
164
165 let token = create_ssh_token(client, app_id.map(|x| x.to_owned())).await?;
167
168 Ok(AcquiredToken {
169 token,
170 app,
171 is_new: true,
172 })
173}
174
175async fn create_ssh_token(
177 client: &WasmerClient,
178 app: Option<String>,
179) -> Result<RawToken, anyhow::Error> {
180 wasmer_backend_api::query::generate_ssh_token(client, app)
181 .await
182 .context("Could not create token for ssh access")
183}