mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-11 22:53:00 +00:00
Add Daytona volume mount passthrough (#263)
## Summary This adds run-configuration support for mounting existing Daytona volumes into Fabro-managed Daytona sandboxes. Concretely, this PR: - adds `[[run.sandbox.daytona.volumes]]` with `volume_id`, `mount_path`, and optional `subpath` - resolves that config through the layer/settings/runtime pipeline - forwards configured mounts to `daytona_sdk::SandboxBaseParams.volumes` when creating the sandbox - documents the configuration surface in the Daytona environment and run configuration docs ## Motivation Daytona already supports attaching volumes when a sandbox is created, but Fabro currently owns that sandbox creation call. That means users cannot attach a pre-created Daytona volume to a Fabro-managed sandbox from run config. The intended use is persistent, provider-owned state such as agent credentials, caches, datasets, or other files that should survive ephemeral sandbox lifecycles. ## Scope This is intentionally a narrow passthrough. Fabro does not create, delete, list, wait on, or otherwise manage Daytona volume lifecycle. Users create the volume in Daytona first, then reference its `volume_id` from Fabro run config. `volumes` defaults to an empty list in resolved settings for backwards compatibility with existing serialized settings. ## Testing - `cargo test -p fabro-server runtime_daytona_config_preserves_volume_mounts` - `cargo test -p fabro-config resolves_daytona_volume_mounts` - `cargo test -p fabro-sandbox --features daytona volume_mounts` - `cargo test -p fabro-workflow runtime_daytona_config_preserves_volume_mounts` - `cargo check -p fabro-server` --- _Re-opened from #262 (originally by @kimprobably) to land a rustfmt fix — the original PR came from an org-owned fork, which blocks maintainer pushes. Branch is now on the base repo. Original commit preserved; one additional commit fixes rustfmt formatting._ Co-authored-by: Tim Keen <tim@keen.digital>
This commit is contained in:
parent
c0fe29390a
commit
e7e4fb5ca1
14 changed files with 248 additions and 27 deletions
|
|
@ -160,6 +160,22 @@ dockerfile = "FROM rust:1.85-slim-bookworm\nRUN apt-get update && apt-get instal
|
|||
|
||||
If the snapshot already exists and is in `Active` state, Fabro uses it directly. If it's in `Building` or `Pending` state, Fabro polls with exponential backoff until it's ready.
|
||||
|
||||
### Volumes
|
||||
|
||||
Mount existing Daytona volumes into each sandbox at creation time:
|
||||
|
||||
```toml title="run.toml"
|
||||
[run.sandbox]
|
||||
provider = "daytona"
|
||||
|
||||
[[run.sandbox.daytona.volumes]]
|
||||
volume_id = "vol-agent-state"
|
||||
mount_path = "/home/daytona/agent-state"
|
||||
subpath = "agent-auth"
|
||||
```
|
||||
|
||||
Fabro does not create or manage Daytona volumes. Create the volume in Daytona first, then reference its `volume_id` from the run config. Use `subpath` when a shared volume should expose only one prefix to the sandbox.
|
||||
|
||||
### Labels
|
||||
|
||||
Attach key-value labels to sandboxes for filtering and identification in the Daytona dashboard:
|
||||
|
|
|
|||
|
|
@ -273,6 +273,11 @@ auto_stop_interval = 60
|
|||
project = "fabro"
|
||||
env = "staging"
|
||||
|
||||
[[run.sandbox.daytona.volumes]]
|
||||
volume_id = "vol-agent-state"
|
||||
mount_path = "/home/daytona/agent-state"
|
||||
subpath = "agent-auth"
|
||||
|
||||
[run.sandbox.daytona.snapshot]
|
||||
name = "my-snapshot"
|
||||
cpu = 4
|
||||
|
|
@ -287,6 +292,7 @@ dockerfile = "FROM rust:1.85-slim-bookworm\nRUN apt-get update"
|
|||
|---|---|
|
||||
| `auto_stop_interval` | Minutes of inactivity before the sandbox auto-stops. |
|
||||
| `labels` | Key-value labels attached to the sandbox for filtering and identification. Labels merge across layers (sticky merge-by-key). |
|
||||
| `volumes` | Existing Daytona volumes to mount at sandbox creation. Each entry requires `volume_id` and `mount_path`; `subpath` is optional. Fabro does not create or manage volume lifecycle. |
|
||||
| `snapshot.name` | Snapshot name to create or use for the sandbox. |
|
||||
| `snapshot.cpu` | CPU cores for the snapshot (integer). |
|
||||
| `snapshot.memory` | Memory size using human-readable units: `"8GB"`, `"16GiB"`, or bare integers that default to GB. |
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@ use super::cli::{CliAuthLayer, CliLoggingLayer, CliTargetLayer};
|
|||
use super::features::FeaturesLayer;
|
||||
use super::llm::{CostRates, CredentialRef, HeaderValueRef, ReasoningEffortFeature};
|
||||
use super::run::{
|
||||
DaytonaSnapshotLayer, HookAgentMarker, HookEntry, HookTlsMode, InterviewProviderLayer,
|
||||
ModelRefOrSplice, NotificationProviderLayer, RunArtifactsLayer, RunCheckpointLayer,
|
||||
RunGoalLayer, RunPrepareLayer, ScmGitHubLayer, StringOrSplice,
|
||||
DaytonaSnapshotLayer, DaytonaVolumeLayer, HookAgentMarker, HookEntry, HookTlsMode,
|
||||
InterviewProviderLayer, ModelRefOrSplice, NotificationProviderLayer, RunArtifactsLayer,
|
||||
RunCheckpointLayer, RunGoalLayer, RunPrepareLayer, ScmGitHubLayer, StringOrSplice,
|
||||
};
|
||||
use super::server::{
|
||||
ObjectStoreLocalLayer, ObjectStoreS3Layer, ServerApiLayer, ServerAuthGithubLayer,
|
||||
|
|
@ -43,6 +43,12 @@ impl<T: Combine> Combine for Option<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Combine for Option<Vec<DaytonaVolumeLayer>> {
|
||||
fn combine(self, other: Self) -> Self {
|
||||
self.or(other)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_combine_or_option {
|
||||
($($ty:ty),+ $(,)?) => {
|
||||
$(
|
||||
|
|
|
|||
|
|
@ -26,14 +26,14 @@ pub use log_filter::LogFilter;
|
|||
pub use maps::{MergeMap, ReplaceMap, StickyMap};
|
||||
pub use project::ProjectLayer;
|
||||
pub use run::{
|
||||
DaytonaDockerfileLayer, DaytonaSandboxLayer, DaytonaSnapshotLayer, DockerSandboxLayer,
|
||||
GitAuthorLayer, HookAgentMarker, HookEntry, HookTlsMode, InterviewProviderLayer,
|
||||
InterviewsLayer, McpEntryLayer, ModelRefOrSplice, NotificationProviderLayer,
|
||||
NotificationRouteLayer, PrepareStep, RunAgentLayer, RunArtifactsLayer, RunCheckpointLayer,
|
||||
RunCloneLayer, RunExecutionLayer, RunGitLayer, RunGoalLayer, RunIntegrationsGithubLayer,
|
||||
RunIntegrationsLayer, RunLayer, RunMetaBranchLayer, RunModelControlsLayer, RunModelLayer,
|
||||
RunPrepareLayer, RunPullRequestLayer, RunRunBranchLayer, RunSandboxLayer, RunScmLayer,
|
||||
ScmGitHubLayer, StringOrSplice,
|
||||
DaytonaDockerfileLayer, DaytonaSandboxLayer, DaytonaSnapshotLayer, DaytonaVolumeLayer,
|
||||
DockerSandboxLayer, GitAuthorLayer, HookAgentMarker, HookEntry, HookTlsMode,
|
||||
InterviewProviderLayer, InterviewsLayer, McpEntryLayer, ModelRefOrSplice,
|
||||
NotificationProviderLayer, NotificationRouteLayer, PrepareStep, RunAgentLayer,
|
||||
RunArtifactsLayer, RunCheckpointLayer, RunCloneLayer, RunExecutionLayer, RunGitLayer,
|
||||
RunGoalLayer, RunIntegrationsGithubLayer, RunIntegrationsLayer, RunLayer, RunMetaBranchLayer,
|
||||
RunModelControlsLayer, RunModelLayer, RunPrepareLayer, RunPullRequestLayer, RunRunBranchLayer,
|
||||
RunSandboxLayer, RunScmLayer, ScmGitHubLayer, StringOrSplice,
|
||||
};
|
||||
pub use server::{
|
||||
GithubIntegrationLayer, IntegrationWebhooksLayer, ObjectStoreLocalLayer, ObjectStoreS3Layer,
|
||||
|
|
|
|||
|
|
@ -356,12 +356,24 @@ pub struct DaytonaSandboxLayer {
|
|||
/// Sticky merge-by-key (provider-native labels).
|
||||
#[serde(default, skip_serializing_if = "StickyMap::is_empty")]
|
||||
pub labels: StickyMap<String>,
|
||||
/// Existing Daytona volumes to mount when creating the sandbox.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub volumes: Option<Vec<DaytonaVolumeLayer>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub snapshot: Option<DaytonaSnapshotLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub network: Option<DaytonaNetworkLayer>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DaytonaVolumeLayer {
|
||||
pub volume_id: String,
|
||||
pub mount_path: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub subpath: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DaytonaSnapshotLayer {
|
||||
|
|
|
|||
|
|
@ -42,10 +42,10 @@ pub use layers::{
|
|||
CliAuthLayer, CliExecAgentLayer, CliExecLayer, CliExecModelLayer, CliLayer, CliLoggingLayer,
|
||||
CliOutputLayer, CliTargetLayer, CliUpdatesLayer, CostRates, CredentialRef,
|
||||
CredentialRefParseError, DaytonaDockerfileLayer, DaytonaSandboxLayer, DaytonaSnapshotLayer,
|
||||
DockerSandboxLayer, FeaturesLayer, GitAuthorLayer, GithubIntegrationLayer, HeaderValueRef,
|
||||
HookAgentMarker, HookEntry, HookTlsMode, IntegrationWebhooksLayer, InterviewProviderLayer,
|
||||
InterviewsLayer, LlmLayer, LlmModelFeatures, LlmModelLimits, LogFilter, McpEntryLayer,
|
||||
MergeMap, ModelControls, ModelCostTable, ModelRefOrSplice, ModelSettings,
|
||||
DaytonaVolumeLayer, DockerSandboxLayer, FeaturesLayer, GitAuthorLayer, GithubIntegrationLayer,
|
||||
HeaderValueRef, HookAgentMarker, HookEntry, HookTlsMode, IntegrationWebhooksLayer,
|
||||
InterviewProviderLayer, InterviewsLayer, LlmLayer, LlmModelFeatures, LlmModelLimits, LogFilter,
|
||||
McpEntryLayer, MergeMap, ModelControls, ModelCostTable, ModelRefOrSplice, ModelSettings,
|
||||
NotificationProviderLayer, NotificationRouteLayer, ObjectStoreLocalLayer, ObjectStoreS3Layer,
|
||||
PrepareStep, ProjectLayer, ProviderSettings, ReasoningEffortFeature, ReplaceMap, RunAgentLayer,
|
||||
RunArtifactsLayer, RunCheckpointLayer, RunCloneLayer, RunExecutionLayer, RunGitLayer,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
use fabro_types::settings::InterpString;
|
||||
use fabro_types::settings::run::{
|
||||
ArtifactsSettings, DaytonaSettings, DaytonaSnapshotSettings, DockerSettings, DockerfileSource,
|
||||
GitAuthorSettings, HookDefinition, HookType, InterviewProviderSettings, McpServerSettings,
|
||||
McpTransport, MergeStrategy, NotificationProviderSettings, NotificationRouteSettings,
|
||||
PullRequestSettings, RunAgentSettings, RunBranchSettings, RunCheckpointSettings,
|
||||
RunCloneSettings, RunExecutionSettings, RunGitSettings, RunGoal, RunIntegrationsGithubSettings,
|
||||
RunIntegrationsSettings, RunInterviewsSettings, RunMetaBranchSettings, RunModelControls,
|
||||
RunModelSettings, RunNamespace, RunPrepareSettings, RunSandboxSettings, RunScmSettings,
|
||||
ScmGitHubSettings, TlsMode,
|
||||
ArtifactsSettings, DaytonaSettings, DaytonaSnapshotSettings, DaytonaVolumeSettings,
|
||||
DockerSettings, DockerfileSource, GitAuthorSettings, HookDefinition, HookType,
|
||||
InterviewProviderSettings, McpServerSettings, McpTransport, MergeStrategy,
|
||||
NotificationProviderSettings, NotificationRouteSettings, PullRequestSettings, RunAgentSettings,
|
||||
RunBranchSettings, RunCheckpointSettings, RunCloneSettings, RunExecutionSettings,
|
||||
RunGitSettings, RunGoal, RunIntegrationsGithubSettings, RunIntegrationsSettings,
|
||||
RunInterviewsSettings, RunMetaBranchSettings, RunModelControls, RunModelSettings, RunNamespace,
|
||||
RunPrepareSettings, RunSandboxSettings, RunScmSettings, ScmGitHubSettings, TlsMode,
|
||||
};
|
||||
|
||||
use super::ResolveError;
|
||||
|
|
@ -269,6 +269,17 @@ fn resolve_daytona(daytona: &DaytonaSandboxLayer) -> DaytonaSettings {
|
|||
DaytonaSettings {
|
||||
auto_stop_interval: daytona.auto_stop_interval,
|
||||
labels: daytona.labels.clone().into_inner(),
|
||||
volumes: daytona
|
||||
.volumes
|
||||
.as_deref()
|
||||
.unwrap_or(&[])
|
||||
.iter()
|
||||
.map(|volume| DaytonaVolumeSettings {
|
||||
volume_id: volume.volume_id.clone(),
|
||||
mount_path: volume.mount_path.clone(),
|
||||
subpath: volume.subpath.clone(),
|
||||
})
|
||||
.collect(),
|
||||
snapshot: daytona.snapshot.as_ref().and_then(|snapshot| {
|
||||
snapshot.name.as_ref().map(|name| DaytonaSnapshotSettings {
|
||||
name: name.clone(),
|
||||
|
|
|
|||
|
|
@ -61,6 +61,36 @@ fn resolves_run_defaults_from_empty_settings() {
|
|||
assert!(settings.pull_request.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_daytona_volume_mounts() {
|
||||
let settings = WorkflowSettingsBuilder::from_toml(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[run.sandbox]
|
||||
provider = "daytona"
|
||||
|
||||
[[run.sandbox.daytona.volumes]]
|
||||
volume_id = "vol_auth"
|
||||
mount_path = "/home/daytona/.config"
|
||||
subpath = "agents"
|
||||
"#,
|
||||
)
|
||||
.expect("daytona volume mount should resolve")
|
||||
.run;
|
||||
|
||||
let daytona = settings
|
||||
.sandbox
|
||||
.daytona
|
||||
.as_ref()
|
||||
.expect("daytona settings should resolve");
|
||||
|
||||
assert_eq!(daytona.volumes.len(), 1);
|
||||
assert_eq!(daytona.volumes[0].volume_id, "vol_auth");
|
||||
assert_eq!(daytona.volumes[0].mount_path, "/home/daytona/.config");
|
||||
assert_eq!(daytona.volumes[0].subpath.as_deref(), Some("agents"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_run_level_clone_branch_controls() {
|
||||
let settings = WorkflowSettingsBuilder::from_toml(
|
||||
|
|
|
|||
|
|
@ -16,12 +16,21 @@ use serde::{Deserialize, Serialize};
|
|||
pub struct DaytonaSettings {
|
||||
pub auto_stop_interval: Option<i32>,
|
||||
pub labels: Option<HashMap<String, String>>,
|
||||
#[serde(default)]
|
||||
pub volumes: Vec<DaytonaVolumeMount>,
|
||||
pub snapshot: Option<DaytonaSnapshotSettings>,
|
||||
pub network: Option<DaytonaNetwork>,
|
||||
#[serde(default)]
|
||||
pub skip_clone: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
pub struct DaytonaVolumeMount {
|
||||
pub volume_id: String,
|
||||
pub mount_path: String,
|
||||
pub subpath: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum DaytonaNetwork {
|
||||
Block,
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ pub const REQUIRED_DAYTONA_PERMISSIONS: &[Permissions] = &[
|
|||
|
||||
pub use crate::config::{
|
||||
DaytonaNetwork, DaytonaSettings as DaytonaConfig,
|
||||
DaytonaSnapshotSettings as DaytonaSnapshotConfig, DockerfileSource,
|
||||
DaytonaSnapshotSettings as DaytonaSnapshotConfig, DaytonaVolumeMount, DockerfileSource,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -103,6 +103,20 @@ fn perm_wire_str(permission: Permissions) -> &'static str {
|
|||
}
|
||||
}
|
||||
|
||||
fn volume_mounts_for_create(config: &DaytonaConfig) -> Option<Vec<daytona_sdk::VolumeMount>> {
|
||||
(!config.volumes.is_empty()).then(|| {
|
||||
config
|
||||
.volumes
|
||||
.iter()
|
||||
.map(|volume| daytona_sdk::VolumeMount {
|
||||
volume_id: volume.volume_id.clone(),
|
||||
mount_path: volume.mount_path.clone(),
|
||||
subpath: volume.subpath.clone(),
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a [`daytona_sdk::Client`], forwarding an optional API key from the
|
||||
/// vault so the SDK doesn't have to rely on `DAYTONA_API_KEY` being in the
|
||||
/// process environment.
|
||||
|
|
@ -448,6 +462,7 @@ impl DaytonaSandbox {
|
|||
ephemeral: Some(false),
|
||||
network_block_all,
|
||||
network_allow_list,
|
||||
volumes: volume_mounts_for_create(&self.config),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
|
@ -2168,6 +2183,45 @@ mod tests {
|
|||
assert!(config.snapshot.is_none());
|
||||
assert!(config.auto_stop_interval.is_none());
|
||||
assert!(config.labels.is_none());
|
||||
assert!(config.volumes.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_volume_mounts_from_config() {
|
||||
let config: DaytonaConfig = toml::from_str(
|
||||
r#"
|
||||
[[volumes]]
|
||||
volume_id = "vol_auth"
|
||||
mount_path = "/home/daytona/.config"
|
||||
subpath = "agents"
|
||||
"#,
|
||||
)
|
||||
.expect("volume config should parse");
|
||||
|
||||
assert_eq!(config.volumes, vec![DaytonaVolumeMount {
|
||||
volume_id: "vol_auth".to_string(),
|
||||
mount_path: "/home/daytona/.config".to_string(),
|
||||
subpath: Some("agents".to_string()),
|
||||
}]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_volume_mounts_to_daytona_create_params() {
|
||||
let config = DaytonaConfig {
|
||||
volumes: vec![DaytonaVolumeMount {
|
||||
volume_id: "vol_auth".to_string(),
|
||||
mount_path: "/home/daytona/.config".to_string(),
|
||||
subpath: Some("agents".to_string()),
|
||||
}],
|
||||
..DaytonaConfig::default()
|
||||
};
|
||||
|
||||
let volumes = volume_mounts_for_create(&config).expect("volumes should map");
|
||||
|
||||
assert_eq!(volumes.len(), 1);
|
||||
assert_eq!(volumes[0].volume_id, "vol_auth");
|
||||
assert_eq!(volumes[0].mount_path, "/home/daytona/.config");
|
||||
assert_eq!(volumes[0].subpath.as_deref(), Some("agents"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
|
|||
|
|
@ -1673,6 +1673,7 @@ mod runs {
|
|||
"project".to_string(),
|
||||
"api-server".to_string(),
|
||||
)]),
|
||||
volumes: Vec::new(),
|
||||
snapshot: Some(DaytonaSnapshotSettings {
|
||||
name: "api-server-dev".into(),
|
||||
cpu: Some(4),
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ use fabro_graphviz::render::apply_direction;
|
|||
use fabro_llm::model_test::{ModelTestStatus, run_basic_model_probe};
|
||||
use fabro_model::{Catalog, ProviderId};
|
||||
use fabro_sandbox::config::{
|
||||
DaytonaNetwork, DaytonaSnapshotSettings, DockerfileSource as SandboxDockerfileSource,
|
||||
DaytonaNetwork, DaytonaSnapshotSettings, DaytonaVolumeMount,
|
||||
DockerfileSource as SandboxDockerfileSource,
|
||||
};
|
||||
use fabro_sandbox::daytona::DaytonaConfig;
|
||||
use fabro_sandbox::redact::redact_auth_url;
|
||||
|
|
@ -1097,6 +1098,15 @@ fn runtime_daytona_config(settings: &DaytonaSettings, skip_clone: bool) -> Dayto
|
|||
DaytonaConfig {
|
||||
auto_stop_interval: settings.auto_stop_interval,
|
||||
labels: (!settings.labels.is_empty()).then_some(settings.labels.clone()),
|
||||
volumes: settings
|
||||
.volumes
|
||||
.iter()
|
||||
.map(|volume| DaytonaVolumeMount {
|
||||
volume_id: volume.volume_id.clone(),
|
||||
mount_path: volume.mount_path.clone(),
|
||||
subpath: volume.subpath.clone(),
|
||||
})
|
||||
.collect(),
|
||||
snapshot: settings
|
||||
.snapshot
|
||||
.as_ref()
|
||||
|
|
@ -1433,6 +1443,25 @@ enabled = {clone_enabled}
|
|||
(prepared, resolved)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_daytona_config_preserves_volume_mounts() {
|
||||
let settings = DaytonaSettings {
|
||||
volumes: vec![fabro_types::settings::run::DaytonaVolumeSettings {
|
||||
volume_id: "vol_auth".to_string(),
|
||||
mount_path: "/home/daytona/.config".to_string(),
|
||||
subpath: Some("agents".to_string()),
|
||||
}],
|
||||
..DaytonaSettings::default()
|
||||
};
|
||||
|
||||
let config = runtime_daytona_config(&settings, false);
|
||||
|
||||
assert_eq!(config.volumes.len(), 1);
|
||||
assert_eq!(config.volumes[0].volume_id, "vol_auth");
|
||||
assert_eq!(config.volumes[0].mount_path, "/home/daytona/.config");
|
||||
assert_eq!(config.volumes[0].subpath.as_deref(), Some("agents"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_manifest_inlines_project_config_daytona_dockerfile_from_bundle() {
|
||||
let mut manifest = minimal_manifest();
|
||||
|
|
|
|||
|
|
@ -324,10 +324,19 @@ pub struct DockerSettings {
|
|||
pub struct DaytonaSettings {
|
||||
pub auto_stop_interval: Option<i32>,
|
||||
pub labels: HashMap<String, String>,
|
||||
#[serde(default)]
|
||||
pub volumes: Vec<DaytonaVolumeSettings>,
|
||||
pub snapshot: Option<DaytonaSnapshotSettings>,
|
||||
pub network: Option<DaytonaNetworkLayer>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct DaytonaVolumeSettings {
|
||||
pub volume_id: String,
|
||||
pub mount_path: String,
|
||||
pub subpath: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum DockerfileSource {
|
||||
Inline(String),
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ use fabro_interview::{AutoApproveInterviewer, Interviewer};
|
|||
use fabro_mcp::config::{McpServerSettings, McpTransport};
|
||||
use fabro_model::{AgentProfileKind, Catalog, FallbackTarget, Provider, ProviderId, adapter};
|
||||
use fabro_sandbox::config::{
|
||||
DaytonaNetwork, DaytonaSnapshotSettings, DockerfileSource as SandboxDockerfileSource,
|
||||
DaytonaNetwork, DaytonaSnapshotSettings, DaytonaVolumeMount,
|
||||
DockerfileSource as SandboxDockerfileSource,
|
||||
};
|
||||
use fabro_sandbox::daytona::DaytonaConfig;
|
||||
use fabro_sandbox::{DockerSandboxOptions, SandboxProvider, SandboxSpec};
|
||||
|
|
@ -680,6 +681,15 @@ fn runtime_daytona_config(settings: &DaytonaSettings, skip_clone: bool) -> Dayto
|
|||
DaytonaConfig {
|
||||
auto_stop_interval: settings.auto_stop_interval,
|
||||
labels: (!settings.labels.is_empty()).then_some(settings.labels.clone()),
|
||||
volumes: settings
|
||||
.volumes
|
||||
.iter()
|
||||
.map(|volume| DaytonaVolumeMount {
|
||||
volume_id: volume.volume_id.clone(),
|
||||
mount_path: volume.mount_path.clone(),
|
||||
subpath: volume.subpath.clone(),
|
||||
})
|
||||
.collect(),
|
||||
snapshot: settings
|
||||
.snapshot
|
||||
.as_ref()
|
||||
|
|
@ -1117,7 +1127,10 @@ mod tests {
|
|||
use std::time::Duration;
|
||||
|
||||
use chrono::Utc;
|
||||
use fabro_config::{RunCloneLayer, RunExecutionLayer, RunLayer, WorkflowSettingsBuilder};
|
||||
use fabro_config::{
|
||||
DaytonaSandboxLayer, DaytonaVolumeLayer, RunCloneLayer, RunExecutionLayer, RunLayer,
|
||||
RunSandboxLayer, WorkflowSettingsBuilder,
|
||||
};
|
||||
use fabro_model::catalog::LlmCatalogSettings;
|
||||
use fabro_store::Database;
|
||||
use fabro_types::settings::ModelRef;
|
||||
|
|
@ -1232,6 +1245,31 @@ mod tests {
|
|||
assert!(resolve_daytona_config(&settings.run).skip_clone);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_daytona_config_preserves_volume_mounts() {
|
||||
let settings = settings_from_run_layer(RunLayer {
|
||||
sandbox: Some(RunSandboxLayer {
|
||||
daytona: Some(DaytonaSandboxLayer {
|
||||
volumes: Some(vec![DaytonaVolumeLayer {
|
||||
volume_id: "vol_auth".to_string(),
|
||||
mount_path: "/home/daytona/.config".to_string(),
|
||||
subpath: Some("agents".to_string()),
|
||||
}]),
|
||||
..DaytonaSandboxLayer::default()
|
||||
}),
|
||||
..RunSandboxLayer::default()
|
||||
}),
|
||||
..RunLayer::default()
|
||||
});
|
||||
|
||||
let config = resolve_daytona_config(&settings.run);
|
||||
|
||||
assert_eq!(config.volumes.len(), 1);
|
||||
assert_eq!(config.volumes[0].volume_id, "vol_auth");
|
||||
assert_eq!(config.volumes[0].mount_path, "/home/daytona/.config");
|
||||
assert_eq!(config.volumes[0].subpath.as_deref(), Some("agents"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_record_git_options_honor_disabled_run_branch() {
|
||||
let mut settings = WorkflowSettings::default();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue