use std::path::Path;
use crate::error::SailError;
use crate::sailbox::types::VolumeInfo;
pub const SOURCE_SAILBOX_ID_HEADER: &str = "X-Sail-Source-Sailbox-ID";
pub const SOURCE_SAILBOX_AUTH_HEADER: &str = "X-Sail-Source-Sailbox-Auth";
const PROC_CMDLINE: &str = "/proc/cmdline";
const VOLUME_METADATA_FILE: &str = ".sail_volume.json";
pub fn ingress_auth_headers() -> Result<Vec<(String, String)>, SailError> {
headers_from(
std::env::var("SAILBOX_ID").ok(),
std::env::var("SAILBOX_TCP_SPOOLER_AUTH_TOKEN").ok(),
Path::new(PROC_CMDLINE),
)
}
fn headers_from(
id_env: Option<String>,
token_env: Option<String>,
cmdline: &Path,
) -> Result<Vec<(String, String)>, SailError> {
let mut sailbox_id = id_env.unwrap_or_default().trim().to_string();
let mut auth_token = token_env.unwrap_or_default().trim().to_string();
if sailbox_id.is_empty() || auth_token.is_empty() {
let (cmdline_id, cmdline_token) = identity_from_cmdline(cmdline);
if sailbox_id.is_empty() {
sailbox_id = cmdline_id;
}
if auth_token.is_empty() {
auth_token = cmdline_token;
}
}
if sailbox_id.is_empty() || auth_token.is_empty() {
return Err(SailError::InvalidArgument {
message: "ingress auth headers are only available inside a sailbox".to_string(),
});
}
Ok(vec![
(SOURCE_SAILBOX_ID_HEADER.to_string(), sailbox_id),
(SOURCE_SAILBOX_AUTH_HEADER.to_string(), auth_token),
])
}
fn identity_from_cmdline(path: &Path) -> (String, String) {
let Ok(contents) = std::fs::read_to_string(path) else {
return (String::new(), String::new());
};
let mut sailbox_id = String::new();
let mut auth_token = String::new();
for arg in contents.split_whitespace() {
if let Some((key, value)) = arg.split_once('=') {
match key {
"sailbox.id" => sailbox_id = value.trim().to_string(),
"sailbox.tcp_spooler_auth" => auth_token = value.trim().to_string(),
_ => {}
}
}
}
(sailbox_id, auth_token)
}
pub fn volume_from_mount(path: &Path) -> Result<VolumeInfo, SailError> {
let metadata_path = path.join(VOLUME_METADATA_FILE);
let contents =
std::fs::read_to_string(&metadata_path).map_err(|_| SailError::InvalidArgument {
message: format!("{} is not a Sail volume mount", path.display()),
})?;
let data: serde_json::Value =
serde_json::from_str(&contents).map_err(|_| SailError::InvalidArgument {
message: format!(
"{} is not valid Sail volume metadata",
metadata_path.display()
),
})?;
let backend = data
.get("backend")
.and_then(serde_json::Value::as_str)
.unwrap_or("")
.trim()
.to_string();
if backend != "nfs" {
return Err(SailError::InvalidArgument {
message: format!(
"{} is a {backend:?} Sail volume; only NFS volumes are supported",
path.display()
),
});
}
let parse_time = |key: &str| {
data.get(key)
.and_then(serde_json::Value::as_str)
.and_then(|s| {
time::OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339).ok()
})
};
Ok(VolumeInfo {
volume_id: data
.get("volume_id")
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
.to_string(),
name: data
.get("name")
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
.to_string(),
backend,
status: "ready".to_string(),
created_at: parse_time("created_at"),
updated_at: parse_time("updated_at"),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn env_identity_wins_over_cmdline() {
let dir = tempfile::tempdir().expect("tempdir");
let cmdline = dir.path().join("cmdline");
std::fs::write(
&cmdline,
"sailbox.id=sb-cmdline sailbox.tcp_spooler_auth=tok-cmdline\n",
)
.expect("write");
let headers = headers_from(
Some("sb-env".to_string()),
Some("tok-env".to_string()),
&cmdline,
)
.expect("headers");
assert_eq!(headers[0].1, "sb-env");
assert_eq!(headers[1].1, "tok-env");
}
#[test]
fn cmdline_fallback_parses_boot_identity() {
let dir = tempfile::tempdir().expect("tempdir");
let cmdline = dir.path().join("cmdline");
std::fs::write(
&cmdline,
"console=ttyS0 sailbox.id=sb-c sailbox.vm_instance_id=vm-1 \
sailbox.tcp_spooler_auth=tok-c quiet\n",
)
.expect("write");
let headers =
headers_from( None, None, &cmdline).expect("headers");
assert_eq!(
headers,
vec![
(SOURCE_SAILBOX_ID_HEADER.to_string(), "sb-c".to_string()),
(SOURCE_SAILBOX_AUTH_HEADER.to_string(), "tok-c".to_string()),
]
);
}
#[test]
fn outside_a_guest_errors() {
let dir = tempfile::tempdir().expect("tempdir");
let err = headers_from(
None,
None,
&dir.path().join("missing"),
)
.expect_err("no identity outside a guest");
assert!(err.to_string().contains("inside a sailbox"), "{err}");
}
#[test]
fn volume_from_mount_reads_metadata() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(
dir.path().join(VOLUME_METADATA_FILE),
r#"{"volume_id":"vol_1","name":"shared","backend":"nfs","created_at":"2026-01-02T03:04:05Z"}"#,
)
.expect("write");
let info = volume_from_mount(dir.path()).expect("volume");
assert_eq!(info.volume_id, "vol_1");
assert_eq!(info.status, "ready");
assert!(info.created_at.is_some());
}
#[test]
fn volume_from_mount_rejects_non_volume_dirs() {
let dir = tempfile::tempdir().expect("tempdir");
let err = volume_from_mount(dir.path()).expect_err("no metadata");
assert!(err.to_string().contains("not a Sail volume mount"), "{err}");
}
}