use std::collections::HashMap;
use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf};
use std::time::Duration;
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use serde::{Deserialize, Serialize};
use crate::error::{VmRuntimeError, VmRuntimeResult};
pub const DEFAULT_GUEST_METADATA_PORT: u32 = 5555;
pub const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
pub const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(5);
const CONNECT_POLL_INTERVAL: Duration = Duration::from_millis(50);
#[derive(Debug, Clone)]
pub struct GuestMetadataConfig {
pub port: u32,
pub connect_timeout: Duration,
pub request_timeout: Duration,
}
impl Default for GuestMetadataConfig {
fn default() -> Self {
Self {
port: DEFAULT_GUEST_METADATA_PORT,
connect_timeout: DEFAULT_CONNECT_TIMEOUT,
request_timeout: DEFAULT_REQUEST_TIMEOUT,
}
}
}
impl GuestMetadataConfig {
pub fn from_env() -> Self {
let defaults = Self::default();
let port = std::env::var("MICROVM_GUEST_METADATA_PORT")
.ok()
.and_then(|v| v.parse::<u32>().ok())
.filter(|v| *v > 0)
.unwrap_or(defaults.port);
let connect_timeout = std::env::var("MICROVM_GUEST_METADATA_CONNECT_TIMEOUT_MS")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.filter(|v| *v > 0)
.map(Duration::from_millis)
.unwrap_or(defaults.connect_timeout);
let request_timeout = std::env::var("MICROVM_GUEST_METADATA_REQUEST_TIMEOUT_MS")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.filter(|v| *v > 0)
.map(Duration::from_millis)
.unwrap_or(defaults.request_timeout);
Self {
port,
connect_timeout,
request_timeout,
}
}
}
#[derive(Debug, Clone)]
pub struct GuestMetadataClient {
uds_path: PathBuf,
config: GuestMetadataConfig,
}
impl GuestMetadataClient {
pub fn new(uds_path: impl Into<PathBuf>, config: GuestMetadataConfig) -> Self {
Self {
uds_path: uds_path.into(),
config,
}
}
pub fn uds_path(&self) -> &Path {
&self.uds_path
}
pub fn config(&self) -> &GuestMetadataConfig {
&self.config
}
pub fn connect(&self) -> VmRuntimeResult<GuestMetadataConn> {
let deadline = std::time::Instant::now() + self.config.connect_timeout;
let stream = self.dial_with_retry(deadline)?;
stream
.set_read_timeout(Some(self.config.request_timeout))
.map_err(|e| VmRuntimeError::GuestMetadata(format!("set_read_timeout failed: {e}")))?;
stream
.set_write_timeout(Some(self.config.request_timeout))
.map_err(|e| VmRuntimeError::GuestMetadata(format!("set_write_timeout failed: {e}")))?;
let mut conn = GuestMetadataConn::from_stream(stream)?;
conn.firecracker_connect(self.config.port)?;
Ok(conn)
}
fn dial_with_retry(&self, deadline: std::time::Instant) -> VmRuntimeResult<UnixStream> {
loop {
match UnixStream::connect(&self.uds_path) {
Ok(s) => return Ok(s),
Err(err) => {
let kind = err.kind();
let retryable = matches!(
kind,
std::io::ErrorKind::NotFound | std::io::ErrorKind::ConnectionRefused
);
if !retryable {
return Err(VmRuntimeError::GuestMetadata(format!(
"connect {}: {err}",
self.uds_path.display()
)));
}
if std::time::Instant::now() >= deadline {
return Err(VmRuntimeError::GuestMetadata(format!(
"timed out connecting to {} after {:?}: {err}",
self.uds_path.display(),
self.config.connect_timeout
)));
}
std::thread::sleep(CONNECT_POLL_INTERVAL);
}
}
}
}
}
#[derive(Debug)]
pub struct GuestMetadataConn {
reader: BufReader<UnixStream>,
writer: UnixStream,
}
impl GuestMetadataConn {
fn from_stream(stream: UnixStream) -> VmRuntimeResult<Self> {
let writer = stream.try_clone().map_err(|e| {
VmRuntimeError::GuestMetadata(format!("clone unix stream for writer half: {e}"))
})?;
Ok(Self {
reader: BufReader::new(stream),
writer,
})
}
pub fn peer_uds(&self) -> Option<PathBuf> {
self.reader
.get_ref()
.peer_addr()
.ok()
.and_then(|addr| addr.as_pathname().map(Path::to_path_buf))
}
fn firecracker_connect(&mut self, port: u32) -> VmRuntimeResult<()> {
let req = format!("CONNECT {port}\n");
self.writer
.write_all(req.as_bytes())
.map_err(|e| VmRuntimeError::GuestMetadata(format!("write CONNECT: {e}")))?;
self.writer
.flush()
.map_err(|e| VmRuntimeError::GuestMetadata(format!("flush CONNECT: {e}")))?;
let mut response = String::new();
self.reader
.read_line(&mut response)
.map_err(|e| VmRuntimeError::GuestMetadata(format!("read CONNECT response: {e}")))?;
let trimmed = response.trim_end_matches(['\r', '\n']);
if !trimmed.starts_with("OK ") {
return Err(VmRuntimeError::GuestMetadata(format!(
"firecracker rejected CONNECT {port}: got {trimmed:?}"
)));
}
Ok(())
}
pub fn set_env(&mut self, env: &HashMap<String, String>) -> VmRuntimeResult<()> {
for key in env.keys() {
validate_env_key(key)?;
}
let id = next_id();
let req = Request::SetEnv {
id: &id,
env: env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect(),
};
self.send(&req)?;
self.recv_ok(&id)
}
pub fn set_secret(&mut self, name: &str, value: &[u8]) -> VmRuntimeResult<()> {
validate_secret_name(name)?;
let id = next_id();
let value_b64 = base64_encode(value);
let req = Request::SetSecret {
id: &id,
name,
value_b64: &value_b64,
};
self.send(&req)?;
self.recv_ok(&id)
}
pub fn ping(&mut self) -> VmRuntimeResult<()> {
let id = next_id();
let req = Request::Ping { id: &id };
self.send(&req)?;
self.recv_ok(&id)
}
fn send(&mut self, req: &Request<'_>) -> VmRuntimeResult<()> {
let mut line = serde_json::to_string(req)
.map_err(|e| VmRuntimeError::GuestMetadata(format!("encode request: {e}")))?;
line.push('\n');
self.writer
.write_all(line.as_bytes())
.map_err(|e| VmRuntimeError::GuestMetadata(format!("write request: {e}")))?;
self.writer
.flush()
.map_err(|e| VmRuntimeError::GuestMetadata(format!("flush request: {e}")))?;
Ok(())
}
fn recv_ok(&mut self, expected_id: &str) -> VmRuntimeResult<()> {
let mut line = String::new();
let n = self
.reader
.read_line(&mut line)
.map_err(|e| VmRuntimeError::GuestMetadata(format!("read response: {e}")))?;
if n == 0 {
return Err(VmRuntimeError::GuestMetadata(
"guest closed connection before responding".to_string(),
));
}
let resp: Response = serde_json::from_str(line.trim_end_matches(['\r', '\n']))
.map_err(|e| VmRuntimeError::GuestMetadata(format!("decode response: {e}")))?;
if resp.id != expected_id {
return Err(VmRuntimeError::GuestMetadata(format!(
"response id mismatch: expected {expected_id}, got {}",
resp.id
)));
}
if !resp.ok {
return Err(VmRuntimeError::GuestMetadata(resp.error.unwrap_or_else(
|| "guest reported failure without detail".to_string(),
)));
}
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "snake_case")]
pub enum Request<'a> {
SetEnv {
id: &'a str,
env: HashMap<&'a str, &'a str>,
},
SetSecret {
id: &'a str,
name: &'a str,
value_b64: &'a str,
},
Ping { id: &'a str },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Response {
pub id: String,
pub ok: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "snake_case")]
pub enum OwnedRequest {
SetEnv {
id: String,
env: HashMap<String, String>,
},
SetSecret {
id: String,
name: String,
value_b64: String,
},
Ping {
id: String,
},
}
impl OwnedRequest {
pub fn id(&self) -> &str {
match self {
OwnedRequest::SetEnv { id, .. }
| OwnedRequest::SetSecret { id, .. }
| OwnedRequest::Ping { id } => id,
}
}
}
pub fn validate_env_key(key: &str) -> VmRuntimeResult<()> {
if key.is_empty() {
return Err(VmRuntimeError::GuestMetadata(
"env key cannot be empty".to_string(),
));
}
let mut chars = key.chars();
let first = chars.next().expect("non-empty key");
if !(first.is_ascii_alphabetic() || first == '_') {
return Err(VmRuntimeError::GuestMetadata(format!(
"env key {key:?} must start with [A-Za-z_]"
)));
}
for c in chars {
if !(c.is_ascii_alphanumeric() || c == '_') {
return Err(VmRuntimeError::GuestMetadata(format!(
"env key {key:?} contains invalid character {c:?}"
)));
}
}
Ok(())
}
pub fn validate_secret_name(name: &str) -> VmRuntimeResult<()> {
if name.is_empty() {
return Err(VmRuntimeError::GuestMetadata(
"secret name cannot be empty".to_string(),
));
}
for c in name.chars() {
if !(c.is_ascii_alphanumeric() || c == '_' || c == '-') {
return Err(VmRuntimeError::GuestMetadata(format!(
"secret name {name:?} contains invalid character {c:?}"
)));
}
}
Ok(())
}
pub fn base64_encode(bytes: &[u8]) -> String {
BASE64_STANDARD.encode(bytes)
}
pub fn base64_decode(input: &str) -> VmRuntimeResult<Vec<u8>> {
BASE64_STANDARD
.decode(input.as_bytes())
.map_err(|e| VmRuntimeError::GuestMetadata(format!("base64 decode: {e}")))
}
fn next_id() -> String {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0);
let tid = thread_id_u64();
format!("{tid:x}-{nanos:08x}-{n:x}")
}
fn thread_id_u64() -> u64 {
let dbg = format!("{:?}", std::thread::current().id());
dbg.trim_start_matches("ThreadId(")
.trim_end_matches(')')
.parse()
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::UnixListener;
use std::path::PathBuf;
use std::sync::mpsc;
use std::thread;
use tempfile::TempDir;
fn expected_connect_line(port: u32) -> String {
format!("CONNECT {port}\n")
}
struct FakeDaemon {
uds_path: PathBuf,
env_file: PathBuf,
secrets_dir: PathBuf,
_tmp: TempDir,
handle: Option<thread::JoinHandle<()>>,
}
impl FakeDaemon {
fn spawn() -> Self {
let tmp = TempDir::new().unwrap();
let uds_path = tmp.path().join("vsock.uds");
let env_file = tmp.path().join("env");
let secrets_dir = tmp.path().join("secrets");
std::fs::create_dir_all(&secrets_dir).unwrap();
let listener = UnixListener::bind(&uds_path).unwrap();
let env_file_clone = env_file.clone();
let secrets_dir_clone = secrets_dir.clone();
let handle = thread::spawn(move || {
let (stream, _) = match listener.accept() {
Ok(p) => p,
Err(_) => return,
};
serve_one_connection(stream, env_file_clone, secrets_dir_clone);
});
Self {
uds_path,
env_file,
secrets_dir,
_tmp: tmp,
handle: Some(handle),
}
}
fn join(&mut self) {
if let Some(h) = self.handle.take() {
let _ = h.join();
}
}
}
fn serve_one_connection(
stream: std::os::unix::net::UnixStream,
env_file: PathBuf,
secrets_dir: PathBuf,
) {
let mut writer = stream.try_clone().expect("clone stream");
let mut reader = BufReader::new(stream);
let mut connect_line = String::new();
if reader.read_line(&mut connect_line).unwrap_or(0) == 0 {
return;
}
assert!(connect_line.starts_with("CONNECT "));
let port: u32 = connect_line
.trim()
.trim_start_matches("CONNECT ")
.parse()
.expect("parse port");
writer.write_all(format!("OK {port}\n").as_bytes()).unwrap();
writer.flush().unwrap();
loop {
let mut line = String::new();
let n = match reader.read_line(&mut line) {
Ok(n) => n,
Err(_) => return,
};
if n == 0 {
return;
}
let req: OwnedRequest = match serde_json::from_str(line.trim_end()) {
Ok(r) => r,
Err(e) => {
let resp = Response {
id: "?".into(),
ok: false,
error: Some(format!("decode: {e}")),
};
write_response(&mut writer, &resp);
continue;
}
};
let resp = match handle_request(req, &env_file, &secrets_dir) {
Ok(id) => Response {
id,
ok: true,
error: None,
},
Err((id, err)) => Response {
id,
ok: false,
error: Some(err),
},
};
write_response(&mut writer, &resp);
}
}
fn write_response(w: &mut impl Write, resp: &Response) {
let mut s = serde_json::to_string(resp).expect("encode");
s.push('\n');
let _ = w.write_all(s.as_bytes());
let _ = w.flush();
}
fn handle_request(
req: OwnedRequest,
env_file: &Path,
secrets_dir: &Path,
) -> Result<String, (String, String)> {
match req {
OwnedRequest::Ping { id } => Ok(id),
OwnedRequest::SetEnv { id, env } => {
let mut buf = String::new();
let mut keys: Vec<&String> = env.keys().collect();
keys.sort();
for k in keys {
let v = &env[k];
if v.is_empty() {
continue;
}
buf.push_str(k);
buf.push('=');
buf.push_str(v);
buf.push('\n');
}
std::fs::write(env_file, buf).map_err(|e| (id.clone(), e.to_string()))?;
Ok(id)
}
OwnedRequest::SetSecret {
id,
name,
value_b64,
} => {
let bytes = base64_decode(&value_b64).map_err(|e| (id.clone(), e.to_string()))?;
let path = secrets_dir.join(&name);
std::fs::write(&path, &bytes).map_err(|e| (id.clone(), e.to_string()))?;
Ok(id)
}
}
}
#[test]
fn config_defaults_match_constants() {
let cfg = GuestMetadataConfig::default();
assert_eq!(cfg.port, DEFAULT_GUEST_METADATA_PORT);
assert_eq!(cfg.connect_timeout, DEFAULT_CONNECT_TIMEOUT);
assert_eq!(cfg.request_timeout, DEFAULT_REQUEST_TIMEOUT);
}
#[test]
fn validate_env_key_accepts_typical_names() {
for k in ["FOO", "FOO_BAR", "_PRIVATE", "X1", "lowercase_ok"] {
validate_env_key(k).unwrap();
}
}
#[test]
fn validate_env_key_rejects_invalid_names() {
for k in ["", "1FOO", "FOO-BAR", "FOO BAR", "FOO.BAR", "FOO=", "../X"] {
assert!(
validate_env_key(k).is_err(),
"expected {k:?} to be rejected"
);
}
}
#[test]
fn validate_secret_name_accepts_typical_names() {
for n in [
"sidecar_token",
"cert-chain",
"abc",
"ABC_123-xyz",
"single",
] {
validate_secret_name(n).unwrap();
}
}
#[test]
fn validate_secret_name_rejects_path_traversal_and_special_chars() {
for n in [
"",
"..",
"../etc/shadow",
"a/b",
"a\\b",
".hidden",
"with space",
"weird;char",
"dotted.name",
] {
assert!(
validate_secret_name(n).is_err(),
"expected {n:?} to be rejected"
);
}
}
#[test]
fn base64_round_trip_matches_rfc4648_vectors() {
let cases: &[(&[u8], &str)] = &[
(b"", ""),
(b"f", "Zg=="),
(b"fo", "Zm8="),
(b"foo", "Zm9v"),
(b"foob", "Zm9vYg=="),
(b"fooba", "Zm9vYmE="),
(b"foobar", "Zm9vYmFy"),
];
for (raw, encoded) in cases {
assert_eq!(base64_encode(raw), *encoded, "encode {raw:?}");
assert_eq!(base64_decode(encoded).unwrap(), *raw, "decode {encoded}");
}
}
#[test]
fn base64_decode_rejects_invalid_character() {
let err = base64_decode("Zm9v$g==").unwrap_err();
assert!(matches!(err, VmRuntimeError::GuestMetadata(_)));
}
#[test]
fn base64_decode_rejects_bad_length() {
let err = base64_decode("Z").unwrap_err();
assert!(matches!(err, VmRuntimeError::GuestMetadata(_)));
}
#[test]
fn request_json_round_trips_through_serde() {
let mut env = HashMap::new();
env.insert("FOO", "bar");
env.insert("BAZ", "qux");
let req = Request::SetEnv { id: "abc", env };
let s = serde_json::to_string(&req).unwrap();
let parsed: OwnedRequest = serde_json::from_str(&s).unwrap();
match parsed {
OwnedRequest::SetEnv { id, env } => {
assert_eq!(id, "abc");
assert_eq!(env.get("FOO").map(String::as_str), Some("bar"));
assert_eq!(env.get("BAZ").map(String::as_str), Some("qux"));
}
other => panic!("unexpected variant: {other:?}"),
}
}
#[test]
fn ping_request_serializes_with_op_tag() {
let req = Request::Ping { id: "p-1" };
let s = serde_json::to_string(&req).unwrap();
assert!(s.contains("\"op\":\"ping\""), "got {s}");
assert!(s.contains("\"id\":\"p-1\""), "got {s}");
}
#[test]
fn set_secret_request_uses_value_b64_field() {
let req = Request::SetSecret {
id: "s-1",
name: "tok",
value_b64: "Zm9v",
};
let s = serde_json::to_string(&req).unwrap();
assert!(s.contains("\"value_b64\":\"Zm9v\""), "got {s}");
}
#[test]
fn response_round_trips_with_optional_error() {
let resp_ok = Response {
id: "x".into(),
ok: true,
error: None,
};
let s = serde_json::to_string(&resp_ok).unwrap();
assert!(!s.contains("error"), "ok response should omit error: {s}");
let parsed: Response = serde_json::from_str(&s).unwrap();
assert!(parsed.ok);
assert_eq!(parsed.id, "x");
let resp_err = Response {
id: "y".into(),
ok: false,
error: Some("nope".into()),
};
let s = serde_json::to_string(&resp_err).unwrap();
let parsed: Response = serde_json::from_str(&s).unwrap();
assert!(!parsed.ok);
assert_eq!(parsed.error.as_deref(), Some("nope"));
}
#[test]
fn set_env_validates_keys_before_sending() {
let mut env = HashMap::new();
env.insert("bad-key".to_string(), "v".to_string());
let client = GuestMetadataClient::new(
"/does/not/exist/vsock.uds",
GuestMetadataConfig {
connect_timeout: Duration::from_millis(0),
request_timeout: Duration::from_millis(50),
..GuestMetadataConfig::default()
},
);
let err = validate_env_key("bad-key").unwrap_err();
assert!(matches!(err, VmRuntimeError::GuestMetadata(_)));
assert_eq!(
client.uds_path(),
std::path::Path::new("/does/not/exist/vsock.uds")
);
}
#[test]
fn connect_returns_timeout_error_when_uds_missing() {
let tmp = TempDir::new().unwrap();
let nonexistent = tmp.path().join("never-bound.uds");
let client = GuestMetadataClient::new(
nonexistent,
GuestMetadataConfig {
connect_timeout: Duration::from_millis(100),
..GuestMetadataConfig::default()
},
);
let err = client.connect().unwrap_err();
match err {
VmRuntimeError::GuestMetadata(msg) => {
assert!(
msg.contains("timed out") || msg.contains("connect"),
"unexpected message: {msg}"
);
}
other => panic!("unexpected error variant: {other:?}"),
}
}
#[test]
fn end_to_end_set_env_writes_env_file() {
let mut daemon = FakeDaemon::spawn();
let client = GuestMetadataClient::new(
daemon.uds_path.clone(),
GuestMetadataConfig {
connect_timeout: Duration::from_secs(2),
request_timeout: Duration::from_secs(2),
..GuestMetadataConfig::default()
},
);
let mut conn = client.connect().expect("connect");
let mut env = HashMap::new();
env.insert("FOO".to_string(), "bar".to_string());
env.insert("BAZ".to_string(), "qux".to_string());
conn.set_env(&env).expect("set_env");
drop(conn);
daemon.join();
let contents = std::fs::read_to_string(&daemon.env_file).unwrap();
assert_eq!(contents, "BAZ=qux\nFOO=bar\n");
}
#[test]
fn end_to_end_set_secret_writes_secret_file() {
let mut daemon = FakeDaemon::spawn();
let client = GuestMetadataClient::new(
daemon.uds_path.clone(),
GuestMetadataConfig {
connect_timeout: Duration::from_secs(2),
request_timeout: Duration::from_secs(2),
..GuestMetadataConfig::default()
},
);
let mut conn = client.connect().expect("connect");
let payload = b"\x00\x01opaque-bytes\xff";
conn.set_secret("sidecar_token", payload)
.expect("set_secret");
drop(conn);
daemon.join();
let path = daemon.secrets_dir.join("sidecar_token");
let contents = std::fs::read(&path).unwrap();
assert_eq!(contents, payload);
}
#[test]
fn end_to_end_ping_round_trips() {
let mut daemon = FakeDaemon::spawn();
let client = GuestMetadataClient::new(
daemon.uds_path.clone(),
GuestMetadataConfig {
connect_timeout: Duration::from_secs(2),
request_timeout: Duration::from_secs(2),
..GuestMetadataConfig::default()
},
);
let mut conn = client.connect().expect("connect");
conn.ping().expect("ping");
conn.ping().expect("second ping reuses connection");
drop(conn);
daemon.join();
}
#[test]
fn end_to_end_firecracker_handshake_uses_configured_port() {
let tmp = TempDir::new().unwrap();
let uds_path = tmp.path().join("vsock.uds");
let listener = UnixListener::bind(&uds_path).unwrap();
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let (stream, _) = listener.accept().unwrap();
let mut writer = stream.try_clone().unwrap();
let mut reader = BufReader::new(stream);
let mut line = String::new();
reader.read_line(&mut line).unwrap();
tx.send(line.clone()).unwrap();
let port: u32 = line
.trim()
.trim_start_matches("CONNECT ")
.parse()
.unwrap_or(0);
writer.write_all(format!("OK {port}\n").as_bytes()).unwrap();
});
let client = GuestMetadataClient::new(
uds_path,
GuestMetadataConfig {
port: 9999,
connect_timeout: Duration::from_secs(2),
request_timeout: Duration::from_secs(2),
},
);
let _ = client.connect().expect("connect");
let observed = rx.recv_timeout(Duration::from_secs(2)).unwrap();
assert_eq!(observed, expected_connect_line(9999));
}
#[test]
fn connect_propagates_rejected_handshake() {
let tmp = TempDir::new().unwrap();
let uds_path = tmp.path().join("vsock.uds");
let listener = UnixListener::bind(&uds_path).unwrap();
thread::spawn(move || {
let (stream, _) = listener.accept().unwrap();
let mut writer = stream.try_clone().unwrap();
let mut reader = BufReader::new(stream);
let mut line = String::new();
let _ = reader.read_line(&mut line);
let _ = writer.write_all(b"REJECT busy\n");
let _ = writer.flush();
});
let client = GuestMetadataClient::new(
uds_path,
GuestMetadataConfig {
connect_timeout: Duration::from_secs(2),
request_timeout: Duration::from_secs(2),
..GuestMetadataConfig::default()
},
);
let err = client.connect().unwrap_err();
match err {
VmRuntimeError::GuestMetadata(msg) => {
assert!(msg.contains("REJECT"), "unexpected message: {msg}");
}
other => panic!("unexpected error variant: {other:?}"),
}
}
#[test]
fn from_env_reads_configured_values() {
unsafe {
std::env::set_var("MICROVM_GUEST_METADATA_PORT", "7777");
std::env::set_var("MICROVM_GUEST_METADATA_CONNECT_TIMEOUT_MS", "1500");
std::env::set_var("MICROVM_GUEST_METADATA_REQUEST_TIMEOUT_MS", "250");
}
let cfg = GuestMetadataConfig::from_env();
unsafe {
std::env::remove_var("MICROVM_GUEST_METADATA_PORT");
std::env::remove_var("MICROVM_GUEST_METADATA_CONNECT_TIMEOUT_MS");
std::env::remove_var("MICROVM_GUEST_METADATA_REQUEST_TIMEOUT_MS");
}
assert_eq!(cfg.port, 7777);
assert_eq!(cfg.connect_timeout, Duration::from_millis(1500));
assert_eq!(cfg.request_timeout, Duration::from_millis(250));
}
#[test]
fn owned_request_id_returns_correct_field() {
let r = OwnedRequest::Ping { id: "a".into() };
assert_eq!(r.id(), "a");
let r = OwnedRequest::SetEnv {
id: "b".into(),
env: HashMap::new(),
};
assert_eq!(r.id(), "b");
let r = OwnedRequest::SetSecret {
id: "c".into(),
name: "n".into(),
value_b64: "".into(),
};
assert_eq!(r.id(), "c");
}
}