Skip to content

Commit 09f788c

Browse files
committed
feat(cli): authenticate to a VMM with a bearer token
1 parent 02d5307 commit 09f788c

2 files changed

Lines changed: 73 additions & 16 deletions

File tree

dstack/crates/dstack-cli-core/src/vmm.rs

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
use anyhow::{anyhow, bail, Result};
1212
use dstack_vmm_rpc::vmm_client::VmmClient;
1313
use dstack_vmm_rpc::{Id, StatusRequest, StatusResponse, VmConfiguration};
14-
use http_client::http_request;
14+
use http_client::http_request_with_headers;
1515
use http_client::prpc::PrpcClient;
1616

1717
/// default local VMM control socket (created by `dstackup install`).
@@ -20,25 +20,46 @@ pub const DEFAULT_HOST: &str = "unix:/var/run/dstack/vmm.sock";
2020
/// a connection to a VMM — local unix socket or remote http endpoint.
2121
pub struct Vmm {
2222
rpc: VmmClient<PrpcClient>,
23-
/// base string usable with [`http_request`] for non-prpc endpoints.
23+
/// base string usable with [`http_request_with_headers`] for non-prpc endpoints.
2424
base: String,
25+
/// bearer token sent with every request when the VMM has `[auth]` enabled.
26+
auth_token: Option<String>,
2527
}
2628

2729
impl Vmm {
2830
/// connect to a VMM addressed by `host`:
2931
/// `unix:/path/to/vmm.sock` (local) or `http(s)://host:port` (remote).
3032
pub fn connect(host: &str) -> Result<Self> {
33+
Self::connect_with_token(host, None)
34+
}
35+
36+
/// like [`Vmm::connect`], additionally sending `Authorization: Bearer
37+
/// <token>` with every request — required when the VMM has `[auth]`
38+
/// enabled. An empty or `None` token sends no header.
39+
pub fn connect_with_token(host: &str, token: Option<&str>) -> Result<Self> {
3140
let host = host.trim();
41+
let token = token.map(str::trim).filter(|t| !t.is_empty());
42+
let with_token = |client: PrpcClient| match token {
43+
Some(token) => client.with_bearer_token(token),
44+
None => client,
45+
};
3246
if let Some(sock) = host.strip_prefix("unix:") {
33-
let rpc = VmmClient::new(PrpcClient::new_unix(sock.to_string(), "/prpc".to_string()));
47+
let client = PrpcClient::new_unix(sock.to_string(), "/prpc".to_string());
48+
let rpc = VmmClient::new(with_token(client));
3449
Ok(Self {
3550
rpc,
3651
base: format!("unix:{sock}"),
52+
auth_token: token.map(str::to_string),
3753
})
3854
} else if host.starts_with("http://") || host.starts_with("https://") {
3955
let base = host.trim_end_matches('/').to_string();
40-
let rpc = VmmClient::new(PrpcClient::new(format!("{base}/prpc")));
41-
Ok(Self { rpc, base })
56+
let client = PrpcClient::new(format!("{base}/prpc"));
57+
let rpc = VmmClient::new(with_token(client));
58+
Ok(Self {
59+
rpc,
60+
base,
61+
auth_token: token.map(str::to_string),
62+
})
4263
} else {
4364
bail!(
4465
"unsupported host '{host}': expected unix:/path/to/vmm.sock or http(s)://host:port"
@@ -112,7 +133,14 @@ impl Vmm {
112133
/// socket and over the localhost HTTP endpoint written by `dstackup install`.
113134
pub async fn logs(&self, id: &str, lines: u32) -> Result<String> {
114135
let path = format!("/logs?id={id}&follow=false&ansi=false&lines={lines}");
115-
let (status, body) = http_request("GET", &self.base, &path, b"").await?;
136+
let auth_header;
137+
let mut headers: Vec<(&str, &str)> = Vec::new();
138+
if let Some(token) = &self.auth_token {
139+
auth_header = format!("Bearer {token}");
140+
headers.push(("Authorization", auth_header.as_str()));
141+
}
142+
let (status, body) =
143+
http_request_with_headers("GET", &self.base, &path, b"", &headers).await?;
116144
if status != 200 {
117145
bail!("vmm /logs returned status {status}");
118146
}

dstack/crates/dstack-cli/src/main.rs

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ struct Cli {
3232
#[arg(long, global = true, value_name = "DIR")]
3333
prefix: Option<String>,
3434

35-
/// auth token for a remote VMM.
35+
/// auth token for a VMM with `[auth]` enabled (sent as `Authorization:
36+
/// Bearer`). Falls back to `DSTACK_VMM_TOKEN`, then the token written by
37+
/// `dstackup install`.
3638
#[arg(long, global = true)]
3739
token: Option<String>,
3840

@@ -105,20 +107,31 @@ enum Command {
105107
#[tokio::main]
106108
async fn main() -> Result<()> {
107109
let cli = Cli::parse();
108-
// remote-auth wiring lands with the TLS+token transport.
109-
let _ = &cli.token;
110110
let defaults = LocalDefaults::read(cli.prefix.as_deref());
111111
let use_local_defaults = cli.host.is_none();
112112
let host = cli
113113
.host
114114
.clone()
115115
.or_else(|| defaults.as_ref().and_then(|d| d.client_url.clone()))
116116
.unwrap_or_else(|| DEFAULT_HOST.to_string());
117+
// auth token: --token, then DSTACK_VMM_TOKEN, then the token file written
118+
// by `dstackup install` (local defaults only).
119+
let token = cli
120+
.token
121+
.clone()
122+
.or_else(|| std::env::var("DSTACK_VMM_TOKEN").ok())
123+
.filter(|t| !t.trim().is_empty())
124+
.or_else(|| {
125+
use_local_defaults
126+
.then(|| defaults.as_ref().and_then(LocalDefaults::token))
127+
.flatten()
128+
});
129+
let token = token.as_deref();
117130
let json = cli.json;
118131

119132
match cli.command {
120-
Command::Apps => cmd_apps(&host, json).await,
121-
Command::Logs { id, lines } => cmd_logs(&host, &id, lines).await,
133+
Command::Apps => cmd_apps(&host, token, json).await,
134+
Command::Logs { id, lines } => cmd_logs(&host, token, &id, lines).await,
122135
Command::Deploy {
123136
compose,
124137
compose_file,
@@ -149,6 +162,7 @@ async fn main() -> Result<()> {
149162
};
150163
cmd_deploy(
151164
&host,
165+
token,
152166
&compose,
153167
&name,
154168
image.as_deref(),
@@ -178,6 +192,7 @@ fn resolve_compose_arg(positional: Option<String>, flagged: Option<String>) -> R
178192

179193
struct LocalDefaults {
180194
client_url: Option<String>,
195+
client_token_path: Option<String>,
181196
image: Option<String>,
182197
allowlist_path: Option<String>,
183198
}
@@ -197,6 +212,11 @@ impl LocalDefaults {
197212
.and_then(|x| x.as_str())
198213
.filter(|s| !s.is_empty())
199214
.map(str::to_string),
215+
client_token_path: v
216+
.get("client_token_path")
217+
.and_then(|x| x.as_str())
218+
.filter(|s| !s.is_empty())
219+
.map(str::to_string),
200220
image: v
201221
.get("image")
202222
.and_then(|x| x.as_str())
@@ -213,6 +233,14 @@ impl LocalDefaults {
213233
fn allowlist_path(&self) -> Option<String> {
214234
self.allowlist_path.clone()
215235
}
236+
237+
/// read the VMM API token from the file recorded by `dstackup install`.
238+
fn token(&self) -> Option<String> {
239+
let path = self.client_token_path.as_ref()?;
240+
let token = std::fs::read_to_string(path).ok()?;
241+
let token = token.trim();
242+
(!token.is_empty()).then(|| token.to_string())
243+
}
216244
}
217245

218246
#[cfg(test)]
@@ -307,6 +335,7 @@ mod tests {
307335
#[allow(clippy::too_many_arguments)]
308336
async fn cmd_deploy(
309337
host: &str,
338+
token: Option<&str>,
310339
compose_path: &str,
311340
name: &str,
312341
image: Option<&str>,
@@ -339,7 +368,7 @@ async fn cmd_deploy(
339368
..Default::default()
340369
};
341370

342-
let vmm = Vmm::connect(host)?;
371+
let vmm = Vmm::connect_with_token(host, token)?;
343372
let hash = vmm.get_compose_hash(&cfg).await?;
344373
let app_id = short(&hash, 40);
345374
cfg.app_id = Some(app_id.clone());
@@ -429,8 +458,8 @@ fn stub(name: &str) -> Result<()> {
429458
)
430459
}
431460

432-
async fn cmd_apps(host: &str, json: bool) -> Result<()> {
433-
let vmm = Vmm::connect(host)?;
461+
async fn cmd_apps(host: &str, token: Option<&str>, json: bool) -> Result<()> {
462+
let vmm = Vmm::connect_with_token(host, token)?;
434463
let resp = vmm.status().await?;
435464
if json {
436465
let arr: Vec<_> = resp
@@ -470,8 +499,8 @@ async fn cmd_apps(host: &str, json: bool) -> Result<()> {
470499
Ok(())
471500
}
472501

473-
async fn cmd_logs(host: &str, id: &str, lines: u32) -> Result<()> {
474-
let vmm = Vmm::connect(host)?;
502+
async fn cmd_logs(host: &str, token: Option<&str>, id: &str, lines: u32) -> Result<()> {
503+
let vmm = Vmm::connect_with_token(host, token)?;
475504
let logs = vmm.logs(id, lines).await?;
476505
print!("{logs}");
477506
Ok(())

0 commit comments

Comments
 (0)