diff --git a/lib/apps/fabro-server/src/install.rs b/lib/apps/fabro-server/src/install.rs index 5efda3ba0..e4c75a0e5 100644 --- a/lib/apps/fabro-server/src/install.rs +++ b/lib/apps/fabro-server/src/install.rs @@ -1005,13 +1005,12 @@ async fn check_install_daytona_api_key( state: &InstallAppState, api_key: String, ) -> anyhow::Result { - let credentials = DaytonaCredentials { - api_key, - api_url: state.upstreams.daytona_api_base_url.clone(), - organization_id: state.upstreams.daytona_organization_id.clone(), - target: None, - http_client: Some(fabro_http::http_client().context("failed to build HTTP client")?), - }; + let credentials = DaytonaCredentials::new(api_key) + .with_api_url(state.upstreams.daytona_api_base_url.clone()) + .with_organization_id(state.upstreams.daytona_organization_id.clone()) + .with_http_client(Some( + fabro_http::http_client().context("failed to build HTTP client")?, + )); daytona::check_daytona_api_key(&credentials, daytona::DAYTONA_CREDENTIAL_PROBE_TIMEOUT).await } diff --git a/lib/apps/fabro-server/src/server.rs b/lib/apps/fabro-server/src/server.rs index ae7d158d6..a363135f0 100644 --- a/lib/apps/fabro-server/src/server.rs +++ b/lib/apps/fabro-server/src/server.rs @@ -1470,15 +1470,8 @@ impl AppState { /// the server's HTTP client. The process environment is consulted only /// through the configured lookup. pub(crate) fn daytona_credentials(&self, api_key: String) -> DaytonaCredentials { - DaytonaCredentials { - api_key, - api_url: self - .config_env_lookup(EnvVars::DAYTONA_API_URL) - .or_else(|| self.config_env_lookup(EnvVars::DAYTONA_SERVER_URL)), - organization_id: self.config_env_lookup(EnvVars::DAYTONA_ORGANIZATION_ID), - target: None, - http_client: self.http_client().ok(), - } + DaytonaCredentials::from_api_key(api_key, |name| self.config_env_lookup(name)) + .with_http_client(self.http_client().ok()) } /// Everything a reconnect needs to reach a run's provider: the server's @@ -2358,14 +2351,8 @@ fn build_sandbox_inventory( if let Some(daytona) = provider_settings.get(&SandboxProviderKind::DAYTONA) { if let Some(api_key) = daytona_api_key.filter(|_| daytona.enabled) { - let credentials = DaytonaCredentials { - api_key, - api_url: env_lookup(EnvVars::DAYTONA_API_URL) - .or_else(|| env_lookup(EnvVars::DAYTONA_SERVER_URL)), - organization_id: env_lookup(EnvVars::DAYTONA_ORGANIZATION_ID), - target: None, - http_client, - }; + let credentials = DaytonaCredentials::from_api_key(api_key, |name| env_lookup(name)) + .with_http_client(http_client); inventory = inventory.with_lazy( SandboxProviderKind::DAYTONA, daytona.clone(), diff --git a/lib/components/fabro-sandbox/src/daytona.rs b/lib/components/fabro-sandbox/src/daytona.rs index 8a2dcd39d..26a2d5e35 100644 --- a/lib/components/fabro-sandbox/src/daytona.rs +++ b/lib/components/fabro-sandbox/src/daytona.rs @@ -689,14 +689,9 @@ mod tests { #[tokio::test] async fn credential_probe_reports_configured_timeout() { - let credentials = DaytonaCredentials { - api_key: "dtn_test".to_string(), - // A non-routable address: the probe cannot finish within the budget. - api_url: Some("http://10.255.255.1:1/api".to_string()), - organization_id: None, - target: None, - http_client: None, - }; + // A non-routable address: the probe cannot finish within the budget. + let credentials = DaytonaCredentials::new("dtn_test".to_string()) + .with_api_url(Some("http://10.255.255.1:1/api".to_string())); let err = check_daytona_api_key(&credentials, Duration::from_millis(1)) .await .expect_err("probe should time out"); @@ -737,15 +732,9 @@ mod wire_gate { )] fn live_credentials() -> Option { let api_key = std::env::var(EnvVars::DAYTONA_API_KEY).ok()?; - Some(DaytonaCredentials { - api_key, - api_url: std::env::var(EnvVars::DAYTONA_API_URL) - .or_else(|_| std::env::var(EnvVars::DAYTONA_SERVER_URL)) - .ok(), - organization_id: std::env::var(EnvVars::DAYTONA_ORGANIZATION_ID).ok(), - target: None, - http_client: None, - }) + Some(DaytonaCredentials::from_api_key(api_key, |name| { + std::env::var(name).ok() + })) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] diff --git a/lib/components/fabro-sandbox/src/driver.rs b/lib/components/fabro-sandbox/src/driver.rs index 12fb872d8..7acbfe8c9 100644 --- a/lib/components/fabro-sandbox/src/driver.rs +++ b/lib/components/fabro-sandbox/src/driver.rs @@ -38,39 +38,74 @@ pub const PLUGIN_BINARY_PREFIX: &str = "fabro-sandbox"; /// `User-Agent` fabro presents to remote sandbox control planes. pub const USER_AGENT: &str = concat!("fabro-sandbox/", env!("CARGO_PKG_VERSION")); -/// Explicit Daytona credentials. The process environment is never consulted. +/// Explicit Daytona credentials: the SDK's configuration with the API key +/// always present and a `Debug` that never prints it. The process +/// environment is never consulted. #[derive(Clone)] -pub struct DaytonaCredentials { - pub api_key: String, - pub api_url: Option, - pub organization_id: Option, - pub target: Option, - /// Shared HTTP client; tests pass a no-proxy client here. - pub http_client: Option, -} +pub struct DaytonaCredentials(DaytonaConfig); impl DaytonaCredentials { + /// Credentials for `api_key` against Daytona's public control plane, + /// presenting fabro's `User-Agent`. + #[must_use] + pub fn new(api_key: String) -> Self { + Self(DaytonaConfig { + api_key: Some(api_key), + user_agent: Some(USER_AGENT.to_string()), + ..DaytonaConfig::default() + }) + } + /// Credentials for a vault API key, with the control-plane URL and /// organization taken from `lookup` (server configuration, or the /// process environment in a CLI worker). Nothing is read implicitly. pub fn from_api_key(api_key: String, lookup: impl Fn(&str) -> Option) -> Self { - Self { - api_key, - api_url: lookup(EnvVars::DAYTONA_API_URL) - .or_else(|| lookup(EnvVars::DAYTONA_SERVER_URL)), - organization_id: lookup(EnvVars::DAYTONA_ORGANIZATION_ID), - target: None, - http_client: None, - } + Self::new(api_key) + .with_api_url( + lookup(EnvVars::DAYTONA_API_URL).or_else(|| lookup(EnvVars::DAYTONA_SERVER_URL)), + ) + .with_organization_id(lookup(EnvVars::DAYTONA_ORGANIZATION_ID)) + } + + /// The control-plane URL; Daytona's public API when `None`. + #[must_use] + pub fn with_api_url(mut self, api_url: Option) -> Self { + self.0.api_url = api_url; + self + } + + #[must_use] + pub fn with_organization_id(mut self, organization_id: Option) -> Self { + self.0.organization_id = organization_id; + self + } + + /// A shared HTTP client; tests pass a no-proxy client here. + #[must_use] + pub fn with_http_client(mut self, http_client: Option) -> Self { + self.0.http_client = http_client; + self + } + + /// The API key, which every constructor sets. + #[must_use] + pub fn api_key(&self) -> &str { + self.0.api_key.as_deref().unwrap_or_default() + } + + /// The SDK configuration the driver's Daytona provider connects with. + #[must_use] + pub fn config(&self) -> &DaytonaConfig { + &self.0 } } impl std::fmt::Debug for DaytonaCredentials { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("DaytonaCredentials") - .field("api_url", &self.api_url) - .field("organization_id", &self.organization_id) - .field("target", &self.target) + .field("api_url", &self.0.api_url) + .field("organization_id", &self.0.organization_id) + .field("target", &self.0.target) .finish_non_exhaustive() } } @@ -187,17 +222,8 @@ pub async fn connect_provider( .daytona .as_ref() .ok_or(ConnectError::MissingDaytonaCredentials)?; - let config = DaytonaConfig { - api_key: Some(credentials.api_key.clone()), - jwt_token: None, - organization_id: credentials.organization_id.clone(), - api_url: credentials.api_url.clone(), - target: credentials.target.clone(), - http_client: credentials.http_client.clone(), - user_agent: Some(USER_AGENT.to_string()), - }; Arc::new( - DaytonaProvider::connect_explicit(config) + DaytonaProvider::connect_explicit(credentials.config().clone()) .await .map_err(driver)?, ) diff --git a/lib/components/fabro-sandbox/src/provider_sandbox.rs b/lib/components/fabro-sandbox/src/provider_sandbox.rs index aa5069d79..33e032870 100644 --- a/lib/components/fabro-sandbox/src/provider_sandbox.rs +++ b/lib/components/fabro-sandbox/src/provider_sandbox.rs @@ -52,7 +52,7 @@ pub async fn provider_sandbox( .ok_or_else(|| crate::Error::message(MISSING_DAYTONA_CREDENTIALS))?; let plan = daytona::create_plan( Arc::clone(&provider), - credentials.api_key.clone(), + credentials.api_key().to_string(), spec, run_id, ); diff --git a/lib/components/fabro-sandbox/tests/daytona_streaming_live.rs b/lib/components/fabro-sandbox/tests/daytona_streaming_live.rs index 2d43e1d83..47fe1b109 100644 --- a/lib/components/fabro-sandbox/tests/daytona_streaming_live.rs +++ b/lib/components/fabro-sandbox/tests/daytona_streaming_live.rs @@ -533,16 +533,11 @@ mod daytona_streaming_live { reason = "live smoke tests take Daytona credentials from the developer's environment" )] fn live_credentials() -> Result { - Ok(DaytonaCredentials { - api_key: std::env::var(EnvVars::DAYTONA_API_KEY) - .context("DAYTONA_API_KEY must be set")?, - api_url: std::env::var(EnvVars::DAYTONA_API_URL) - .or_else(|_| std::env::var(EnvVars::DAYTONA_SERVER_URL)) - .ok(), - organization_id: std::env::var(EnvVars::DAYTONA_ORGANIZATION_ID).ok(), - target: None, - http_client: None, - }) + let api_key = + std::env::var(EnvVars::DAYTONA_API_KEY).context("DAYTONA_API_KEY must be set")?; + Ok(DaytonaCredentials::from_api_key(api_key, |name| { + std::env::var(name).ok() + })) } fn daytona_access(credentials: DaytonaCredentials) -> ProviderAccess { diff --git a/lib/components/fabro-workflow/tests/it/daytona_integration.rs b/lib/components/fabro-workflow/tests/it/daytona_integration.rs index 1207f2948..02bcb096b 100644 --- a/lib/components/fabro-workflow/tests/it/daytona_integration.rs +++ b/lib/components/fabro-workflow/tests/it/daytona_integration.rs @@ -194,16 +194,8 @@ fn daytona_access(credentials: DaytonaCredentials) -> ProviderAccess { } fn live_daytona_credentials() -> DaytonaCredentials { - DaytonaCredentials { - api_key: std::env::var(EnvVars::DAYTONA_API_KEY) - .expect("DAYTONA_API_KEY must be set"), - api_url: std::env::var(EnvVars::DAYTONA_API_URL) - .or_else(|_| std::env::var(EnvVars::DAYTONA_SERVER_URL)) - .ok(), - organization_id: std::env::var(EnvVars::DAYTONA_ORGANIZATION_ID).ok(), - target: None, - http_client: None, - } + let api_key = std::env::var(EnvVars::DAYTONA_API_KEY).expect("DAYTONA_API_KEY must be set"); + DaytonaCredentials::from_api_key(api_key, |name| std::env::var(name).ok()) } async fn create_env() -> RunSandbox {