mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-09 03:20:56 +00:00
fix(sandbox): surface snapshot lifecycle progress
Emit snapshot slow-path events only when Docker or Daytona actually performs image or snapshot work, replace retired completion markers with snapshot.ready, and render the lifecycle in attach/log output.
This commit is contained in:
parent
fe342a4bd7
commit
446c7cc065
13 changed files with 533 additions and 96 deletions
|
|
@ -151,6 +151,8 @@ Prefer `Emitter::emit(&Event::...)`.
|
|||
|
||||
Use `to_run_event()` only for true bypass paths.
|
||||
|
||||
For cache-backed lifecycle work, emit slow-path start events only when the operation actually misses cache or waits on remote state. Completion events should represent a real ensure step (inspect, build, pull, or poll), not a configured no-op.
|
||||
|
||||
### 7. Update consumers
|
||||
|
||||
Check:
|
||||
|
|
|
|||
|
|
@ -1652,6 +1652,8 @@ Emitted after the engine completes sandbox initialization (distinct from `sandbo
|
|||
|
||||
### `sandbox.snapshot.pulling`
|
||||
|
||||
Emitted only when the Docker image cache misses and Fabro starts pulling the image.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "...", "ts": "...", "run_id": "...",
|
||||
|
|
@ -1666,42 +1668,10 @@ Emitted after the engine completes sandbox initialization (distinct from `sandbo
|
|||
|----------|------|-------------|
|
||||
| `name` | string | Image/snapshot name |
|
||||
|
||||
### `sandbox.snapshot.pulled`
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "...", "ts": "...", "run_id": "...",
|
||||
"event": "sandbox.snapshot.pulled",
|
||||
"properties": {
|
||||
"name": "my-image:latest",
|
||||
"duration_ms": 15000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `name` | string | Image/snapshot name |
|
||||
| `duration_ms` | number | Pull duration |
|
||||
|
||||
### `sandbox.snapshot.ensuring`
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "...", "ts": "...", "run_id": "...",
|
||||
"event": "sandbox.snapshot.ensuring",
|
||||
"properties": {
|
||||
"name": "my-snapshot"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `name` | string | Snapshot name |
|
||||
|
||||
### `sandbox.snapshot.creating`
|
||||
|
||||
Emitted only when a Daytona snapshot cache miss or inactive snapshot requires Fabro to create or wait for the snapshot.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "...", "ts": "...", "run_id": "...",
|
||||
|
|
@ -1718,6 +1688,8 @@ Emitted after the engine completes sandbox initialization (distinct from `sandbo
|
|||
|
||||
### `sandbox.snapshot.ready`
|
||||
|
||||
Emitted when an image or snapshot ensure step succeeds. Cache hits still emit this event with a near-zero `duration_ms`; explicit no-op paths such as Docker `auto_pull = false` and the Daytona default snapshot path do not.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "...", "ts": "...", "run_id": "...",
|
||||
|
|
@ -1732,10 +1704,12 @@ Emitted after the engine completes sandbox initialization (distinct from `sandbo
|
|||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `name` | string | Snapshot name |
|
||||
| `duration_ms` | number | Creation duration |
|
||||
| `duration_ms` | number | Ensure duration |
|
||||
|
||||
### `sandbox.snapshot.failed`
|
||||
|
||||
Emitted when an image or snapshot ensure step fails.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "...", "ts": "...", "run_id": "...",
|
||||
|
|
@ -1751,6 +1725,7 @@ Emitted after the engine completes sandbox initialization (distinct from `sandbo
|
|||
|----------|------|-------------|
|
||||
| `name` | string | Snapshot name |
|
||||
| `error` | string | Error message |
|
||||
| `causes` | string[] | Optional error cause chain |
|
||||
|
||||
### `sandbox.git.started`
|
||||
|
||||
|
|
|
|||
|
|
@ -642,6 +642,43 @@ fn format_event_pretty_value(envelope: &serde_json::Value, styles: &Styles) -> O
|
|||
styles.dim.apply_to(&duration),
|
||||
))
|
||||
}
|
||||
"sandbox.snapshot.pulling" => {
|
||||
let name = prop_str_field(envelope, "name").unwrap_or("?");
|
||||
Some(format!(
|
||||
"{} Sandbox: pulling {}",
|
||||
styles.dim.apply_to(&ts),
|
||||
name,
|
||||
))
|
||||
}
|
||||
"sandbox.snapshot.creating" => {
|
||||
let name = prop_str_field(envelope, "name").unwrap_or("?");
|
||||
Some(format!(
|
||||
"{} Sandbox: building {}",
|
||||
styles.dim.apply_to(&ts),
|
||||
name,
|
||||
))
|
||||
}
|
||||
"sandbox.snapshot.ready" => {
|
||||
let name = prop_str_field(envelope, "name").unwrap_or("?");
|
||||
let duration = format_duration_ms(prop_field(envelope, "duration_ms"));
|
||||
Some(format!(
|
||||
"{} Sandbox snapshot: {} {}",
|
||||
styles.dim.apply_to(&ts),
|
||||
name,
|
||||
styles.dim.apply_to(&duration),
|
||||
))
|
||||
}
|
||||
"sandbox.snapshot.failed" => {
|
||||
let name = prop_str_field(envelope, "name").unwrap_or("?");
|
||||
let error = prop_str_field(envelope, "error").unwrap_or("unknown error");
|
||||
Some(format!(
|
||||
"{} {} Sandbox snapshot {} failed: {}",
|
||||
styles.dim.apply_to(&ts),
|
||||
styles.bold_red.apply_to("\u{2717}"),
|
||||
name,
|
||||
styles.red.apply_to(error),
|
||||
))
|
||||
}
|
||||
"setup.completed" => {
|
||||
let count = prop_field(envelope, "command_count").and_then(serde_json::Value::as_u64);
|
||||
let duration = format_duration_ms(prop_field(envelope, "duration_ms"));
|
||||
|
|
@ -1155,6 +1192,45 @@ mod tests {
|
|||
assert!(result.contains("[push]"), "got: {result}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pretty_sandbox_snapshot_pulling() {
|
||||
let styles = no_color_styles();
|
||||
let line = r#"{"ts":"2026-01-01T14:25:00Z","event":"sandbox.snapshot.pulling","properties":{"name":"buildpack-deps:noble"}}"#;
|
||||
let result = format_event_pretty(line, &styles).unwrap();
|
||||
assert!(result.contains("Sandbox: pulling"), "got: {result}");
|
||||
assert!(result.contains("buildpack-deps:noble"), "got: {result}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pretty_sandbox_snapshot_creating() {
|
||||
let styles = no_color_styles();
|
||||
let line = r#"{"ts":"2026-01-01T14:25:00Z","event":"sandbox.snapshot.creating","properties":{"name":"fabro-v9-test"}}"#;
|
||||
let result = format_event_pretty(line, &styles).unwrap();
|
||||
assert!(result.contains("Sandbox: building"), "got: {result}");
|
||||
assert!(result.contains("fabro-v9-test"), "got: {result}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pretty_sandbox_snapshot_ready() {
|
||||
let styles = no_color_styles();
|
||||
let line = r#"{"ts":"2026-01-01T14:25:00Z","event":"sandbox.snapshot.ready","properties":{"name":"buildpack-deps:noble","duration_ms":8200}}"#;
|
||||
let result = format_event_pretty(line, &styles).unwrap();
|
||||
assert!(result.contains("Sandbox snapshot:"), "got: {result}");
|
||||
assert!(result.contains("buildpack-deps:noble"), "got: {result}");
|
||||
assert!(result.contains("8s"), "got: {result}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pretty_sandbox_snapshot_failed() {
|
||||
let styles = no_color_styles();
|
||||
let line = r#"{"ts":"2026-01-01T14:25:00Z","event":"sandbox.snapshot.failed","properties":{"name":"buildpack-deps:noble","error":"pull failed"}}"#;
|
||||
let result = format_event_pretty(line, &styles).unwrap();
|
||||
assert!(
|
||||
result.contains("Sandbox snapshot buildpack-deps:noble failed: pull failed"),
|
||||
"got: {result}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pretty_stream_suppresses_metadata_compat_notice_only() {
|
||||
let styles = no_color_styles();
|
||||
|
|
|
|||
|
|
@ -55,6 +55,24 @@ pub(super) enum ProgressEvent {
|
|||
memory: Option<f64>,
|
||||
url: Option<String>,
|
||||
},
|
||||
SandboxFailed {
|
||||
provider: String,
|
||||
error: String,
|
||||
},
|
||||
SnapshotPulling {
|
||||
name: String,
|
||||
},
|
||||
SnapshotCreating {
|
||||
name: String,
|
||||
},
|
||||
SnapshotReady {
|
||||
name: String,
|
||||
duration_ms: u64,
|
||||
},
|
||||
SnapshotFailed {
|
||||
name: String,
|
||||
error: String,
|
||||
},
|
||||
SshAccessReady {
|
||||
ssh_command: String,
|
||||
},
|
||||
|
|
@ -252,6 +270,24 @@ pub(super) fn from_run_event(stored: &RunEvent) -> Option<ProgressEvent> {
|
|||
memory: props.memory,
|
||||
url: props.url.clone(),
|
||||
}),
|
||||
EventBody::SandboxFailed(props) => Some(ProgressEvent::SandboxFailed {
|
||||
provider: props.provider.clone(),
|
||||
error: props.error.clone(),
|
||||
}),
|
||||
EventBody::SnapshotPulling(props) => Some(ProgressEvent::SnapshotPulling {
|
||||
name: props.name.clone(),
|
||||
}),
|
||||
EventBody::SnapshotCreating(props) => Some(ProgressEvent::SnapshotCreating {
|
||||
name: props.name.clone(),
|
||||
}),
|
||||
EventBody::SnapshotReady(props) => Some(ProgressEvent::SnapshotReady {
|
||||
name: props.name.clone(),
|
||||
duration_ms: props.duration_ms,
|
||||
}),
|
||||
EventBody::SnapshotFailed(props) => Some(ProgressEvent::SnapshotFailed {
|
||||
name: props.name.clone(),
|
||||
error: props.error.clone(),
|
||||
}),
|
||||
EventBody::SshAccessReady(props) => Some(ProgressEvent::SshAccessReady {
|
||||
ssh_command: props.ssh_command.clone(),
|
||||
}),
|
||||
|
|
@ -698,6 +734,72 @@ mod tests {
|
|||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_sandbox_failed() {
|
||||
let event = Event::Sandbox {
|
||||
event: fabro_agent::SandboxEvent::InitializeFailed {
|
||||
provider: "docker".into(),
|
||||
error: "pull failed".into(),
|
||||
causes: Vec::new(),
|
||||
duration_ms: 900,
|
||||
},
|
||||
};
|
||||
|
||||
let stored = to_run_event(&fixtures::RUN_1, &event);
|
||||
let parsed = from_run_event(&stored).unwrap();
|
||||
assert!(matches!(
|
||||
parsed,
|
||||
ProgressEvent::SandboxFailed { provider, error }
|
||||
if provider == "docker" && error == "pull failed"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_snapshot_lifecycle_events() {
|
||||
let pulling = to_run_event(&fixtures::RUN_1, &Event::Sandbox {
|
||||
event: fabro_agent::SandboxEvent::SnapshotPulling {
|
||||
name: "buildpack-deps:noble".into(),
|
||||
},
|
||||
});
|
||||
let creating = to_run_event(&fixtures::RUN_1, &Event::Sandbox {
|
||||
event: fabro_agent::SandboxEvent::SnapshotCreating {
|
||||
name: "fabro-v9".into(),
|
||||
},
|
||||
});
|
||||
let ready = to_run_event(&fixtures::RUN_1, &Event::Sandbox {
|
||||
event: fabro_agent::SandboxEvent::SnapshotReady {
|
||||
name: "buildpack-deps:noble".into(),
|
||||
duration_ms: 1200,
|
||||
},
|
||||
});
|
||||
let failed = to_run_event(&fixtures::RUN_1, &Event::Sandbox {
|
||||
event: fabro_agent::SandboxEvent::SnapshotFailed {
|
||||
name: "fabro-v9".into(),
|
||||
error: "build failed".into(),
|
||||
causes: Vec::new(),
|
||||
},
|
||||
});
|
||||
|
||||
assert!(matches!(
|
||||
from_run_event(&pulling).unwrap(),
|
||||
ProgressEvent::SnapshotPulling { name } if name == "buildpack-deps:noble"
|
||||
));
|
||||
assert!(matches!(
|
||||
from_run_event(&creating).unwrap(),
|
||||
ProgressEvent::SnapshotCreating { name } if name == "fabro-v9"
|
||||
));
|
||||
assert!(matches!(
|
||||
from_run_event(&ready).unwrap(),
|
||||
ProgressEvent::SnapshotReady { name, duration_ms }
|
||||
if name == "buildpack-deps:noble" && duration_ms == 1200
|
||||
));
|
||||
assert!(matches!(
|
||||
from_run_event(&failed).unwrap(),
|
||||
ProgressEvent::SnapshotFailed { name, error }
|
||||
if name == "fabro-v9" && error == "build failed"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_run_notice() {
|
||||
let event = Event::RunNotice {
|
||||
|
|
|
|||
|
|
@ -122,6 +122,21 @@ impl ProgressUI {
|
|||
ProgressEvent::SandboxInitializing { provider } => {
|
||||
self.setup.on_sandbox_initializing(renderer, &provider);
|
||||
}
|
||||
ProgressEvent::SandboxFailed { provider, error } => {
|
||||
self.setup.on_sandbox_failed(renderer, &provider, &error);
|
||||
}
|
||||
ProgressEvent::SnapshotPulling { name } => {
|
||||
self.setup.on_snapshot_pulling(renderer, &name);
|
||||
}
|
||||
ProgressEvent::SnapshotCreating { name } => {
|
||||
self.setup.on_snapshot_creating(renderer, &name);
|
||||
}
|
||||
ProgressEvent::SnapshotReady { name, duration_ms } => {
|
||||
self.setup.on_snapshot_ready(renderer, &name, duration_ms);
|
||||
}
|
||||
ProgressEvent::SnapshotFailed { name, error } => {
|
||||
self.setup.on_snapshot_failed(renderer, &name, &error);
|
||||
}
|
||||
ProgressEvent::SandboxReady {
|
||||
provider,
|
||||
duration_ms,
|
||||
|
|
@ -882,6 +897,189 @@ mod tests {
|
|||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_daytona_snapshot_creation_snapshot() {
|
||||
let (mut ui, buffer) = capture_ui(false);
|
||||
|
||||
emit(&mut ui, Event::Sandbox {
|
||||
event: SandboxEvent::Initializing {
|
||||
provider: "daytona".into(),
|
||||
},
|
||||
});
|
||||
emit(&mut ui, Event::Sandbox {
|
||||
event: SandboxEvent::SnapshotCreating {
|
||||
name: "fabro-v9-test".into(),
|
||||
},
|
||||
});
|
||||
emit(&mut ui, Event::Sandbox {
|
||||
event: SandboxEvent::SnapshotReady {
|
||||
name: "fabro-v9-test".into(),
|
||||
duration_ms: 210_000,
|
||||
},
|
||||
});
|
||||
emit(&mut ui, Event::Sandbox {
|
||||
event: SandboxEvent::Ready {
|
||||
provider: "daytona".into(),
|
||||
duration_ms: 212_000,
|
||||
name: Some("sandbox-1".into()),
|
||||
cpu: Some(4.0),
|
||||
memory: Some(8.0),
|
||||
url: None,
|
||||
},
|
||||
});
|
||||
|
||||
insta::assert_snapshot!(rendered(&buffer), @r"
|
||||
Sandbox: building fabro-v9-test...
|
||||
Sandbox: daytona (ready in 3m32s)
|
||||
sandbox-1 (4 cpu, 8 GB)
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_docker_snapshot_pull_snapshot() {
|
||||
let (mut ui, buffer) = capture_ui(false);
|
||||
|
||||
emit(&mut ui, Event::Sandbox {
|
||||
event: SandboxEvent::Initializing {
|
||||
provider: "docker".into(),
|
||||
},
|
||||
});
|
||||
emit(&mut ui, Event::Sandbox {
|
||||
event: SandboxEvent::SnapshotPulling {
|
||||
name: "buildpack-deps:noble".into(),
|
||||
},
|
||||
});
|
||||
emit(&mut ui, Event::Sandbox {
|
||||
event: SandboxEvent::SnapshotReady {
|
||||
name: "buildpack-deps:noble".into(),
|
||||
duration_ms: 8_200,
|
||||
},
|
||||
});
|
||||
emit(&mut ui, Event::Sandbox {
|
||||
event: SandboxEvent::Ready {
|
||||
provider: "docker".into(),
|
||||
duration_ms: 9_000,
|
||||
name: None,
|
||||
cpu: None,
|
||||
memory: None,
|
||||
url: None,
|
||||
},
|
||||
});
|
||||
|
||||
insta::assert_snapshot!(rendered(&buffer), @r"
|
||||
Sandbox: pulling buildpack-deps:noble...
|
||||
Sandbox: docker (ready in 9s)
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_docker_skipped_snapshot_phase_snapshot() {
|
||||
let (mut ui, buffer) = capture_ui(false);
|
||||
|
||||
emit(&mut ui, Event::Sandbox {
|
||||
event: SandboxEvent::Initializing {
|
||||
provider: "docker".into(),
|
||||
},
|
||||
});
|
||||
emit(&mut ui, Event::Sandbox {
|
||||
event: SandboxEvent::Ready {
|
||||
provider: "docker".into(),
|
||||
duration_ms: 20,
|
||||
name: None,
|
||||
cpu: None,
|
||||
memory: None,
|
||||
url: None,
|
||||
},
|
||||
});
|
||||
|
||||
insta::assert_snapshot!(rendered(&buffer), @" Sandbox: docker (ready in 20ms)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_snapshot_failure_snapshot() {
|
||||
let (mut ui, buffer) = capture_ui(false);
|
||||
|
||||
emit(&mut ui, Event::Sandbox {
|
||||
event: SandboxEvent::Initializing {
|
||||
provider: "docker".into(),
|
||||
},
|
||||
});
|
||||
emit(&mut ui, Event::Sandbox {
|
||||
event: SandboxEvent::SnapshotFailed {
|
||||
name: "buildpack-deps:noble".into(),
|
||||
error: "pull failed".into(),
|
||||
causes: Vec::new(),
|
||||
},
|
||||
});
|
||||
emit(&mut ui, Event::Sandbox {
|
||||
event: SandboxEvent::InitializeFailed {
|
||||
provider: "docker".into(),
|
||||
error: "pull failed".into(),
|
||||
causes: Vec::new(),
|
||||
duration_ms: 900,
|
||||
},
|
||||
});
|
||||
|
||||
insta::assert_snapshot!(rendered(&buffer), @r"
|
||||
Sandbox: Snapshot buildpack-deps:noble failed: pull failed
|
||||
Sandbox: docker failed: pull failed
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tty_snapshot_ready_keeps_sandbox_bar_until_sandbox_ready() {
|
||||
let mut ui = ProgressUI::new(true, false);
|
||||
|
||||
emit(&mut ui, Event::Sandbox {
|
||||
event: SandboxEvent::Initializing {
|
||||
provider: "docker".into(),
|
||||
},
|
||||
});
|
||||
assert!(ui.setup.sandbox_bar.is_some());
|
||||
|
||||
emit(&mut ui, Event::Sandbox {
|
||||
event: SandboxEvent::SnapshotReady {
|
||||
name: "buildpack-deps:noble".into(),
|
||||
duration_ms: 10,
|
||||
},
|
||||
});
|
||||
assert!(ui.setup.sandbox_bar.is_some());
|
||||
|
||||
emit(&mut ui, Event::Sandbox {
|
||||
event: SandboxEvent::Ready {
|
||||
provider: "docker".into(),
|
||||
duration_ms: 20,
|
||||
name: None,
|
||||
cpu: None,
|
||||
memory: None,
|
||||
url: None,
|
||||
},
|
||||
});
|
||||
assert!(ui.setup.sandbox_bar.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tty_sandbox_failed_finishes_sandbox_bar() {
|
||||
let mut ui = ProgressUI::new(true, false);
|
||||
|
||||
emit(&mut ui, Event::Sandbox {
|
||||
event: SandboxEvent::Initializing {
|
||||
provider: "docker".into(),
|
||||
},
|
||||
});
|
||||
assert!(ui.setup.sandbox_bar.is_some());
|
||||
|
||||
emit(&mut ui, Event::Sandbox {
|
||||
event: SandboxEvent::InitializeFailed {
|
||||
provider: "docker".into(),
|
||||
error: "pull failed".into(),
|
||||
causes: Vec::new(),
|
||||
duration_ms: 900,
|
||||
},
|
||||
});
|
||||
assert!(ui.setup.sandbox_bar.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_verbose_snapshot() {
|
||||
let (mut ui, buffer) = capture_ui(true);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use crate::shared::format_duration_ms;
|
|||
|
||||
pub(super) struct SetupDisplay {
|
||||
verbose: bool,
|
||||
current_provider: Option<String>,
|
||||
pub(super) sandbox_bar: Option<ProgressBar>,
|
||||
pub(super) setup_bar: Option<ProgressBar>,
|
||||
pub(super) setup_command_count: u64,
|
||||
|
|
@ -20,6 +21,7 @@ impl SetupDisplay {
|
|||
pub(super) fn new(verbose: bool) -> Self {
|
||||
Self {
|
||||
verbose,
|
||||
current_provider: None,
|
||||
sandbox_bar: None,
|
||||
setup_bar: None,
|
||||
setup_command_count: 0,
|
||||
|
|
@ -45,10 +47,11 @@ impl SetupDisplay {
|
|||
}
|
||||
|
||||
pub(super) fn on_sandbox_initializing(&mut self, renderer: &ProgressRenderer, provider: &str) {
|
||||
self.current_provider = Some(provider.to_string());
|
||||
if renderer.is_tty() {
|
||||
let bar = renderer.add_spinner();
|
||||
bar.set_style(styles::style_header_running());
|
||||
bar.set_message(format!("Initializing {provider} sandbox..."));
|
||||
bar.set_message(initializing_message(provider));
|
||||
bar.enable_steady_tick(Duration::from_millis(100));
|
||||
self.sandbox_bar = Some(bar);
|
||||
}
|
||||
|
|
@ -99,6 +102,70 @@ impl SetupDisplay {
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) fn on_sandbox_failed(
|
||||
&mut self,
|
||||
renderer: &ProgressRenderer,
|
||||
provider: &str,
|
||||
error: &str,
|
||||
) {
|
||||
if renderer.is_tty() {
|
||||
let bar = self
|
||||
.sandbox_bar
|
||||
.take()
|
||||
.unwrap_or_else(|| renderer.add_spinner());
|
||||
bar.set_style(styles::style_header_failed());
|
||||
bar.finish_with_message(format!("Sandbox: {provider} failed: {error}"));
|
||||
} else {
|
||||
renderer.print_line(4, &format!("Sandbox: {provider} failed: {error}"));
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn on_snapshot_pulling(&self, renderer: &ProgressRenderer, name: &str) {
|
||||
if renderer.is_tty() {
|
||||
if let Some(bar) = self.sandbox_bar.as_ref() {
|
||||
bar.set_message(format!("Pulling {name}..."));
|
||||
}
|
||||
} else {
|
||||
renderer.print_line(4, &format!("Sandbox: pulling {name}..."));
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn on_snapshot_creating(&self, renderer: &ProgressRenderer, name: &str) {
|
||||
if renderer.is_tty() {
|
||||
if let Some(bar) = self.sandbox_bar.as_ref() {
|
||||
bar.set_message(format!("Building {name}..."));
|
||||
}
|
||||
} else {
|
||||
renderer.print_line(4, &format!("Sandbox: building {name}..."));
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn on_snapshot_ready(
|
||||
&self,
|
||||
renderer: &ProgressRenderer,
|
||||
_name: &str,
|
||||
_duration_ms: u64,
|
||||
) {
|
||||
if renderer.is_tty() {
|
||||
if let Some(bar) = self.sandbox_bar.as_ref() {
|
||||
let provider = self.current_provider.as_deref().unwrap_or("sandbox");
|
||||
bar.set_style(styles::style_header_running());
|
||||
bar.set_message(initializing_message(provider));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn on_snapshot_failed(&self, renderer: &ProgressRenderer, name: &str, error: &str) {
|
||||
let message = format!("Snapshot {name} failed: {error}");
|
||||
if renderer.is_tty() {
|
||||
if let Some(bar) = self.sandbox_bar.as_ref() {
|
||||
bar.set_message(message);
|
||||
}
|
||||
} else {
|
||||
renderer.print_line(4, &format!("Sandbox: {message}"));
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn on_ssh_access_ready(renderer: &ProgressRenderer, ssh_command: &str) {
|
||||
if renderer.is_tty() {
|
||||
let bar = renderer.add_spinner();
|
||||
|
|
@ -376,3 +443,11 @@ impl SetupDisplay {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn initializing_message(provider: &str) -> String {
|
||||
if provider == "sandbox" {
|
||||
"Initializing sandbox...".to_string()
|
||||
} else {
|
||||
format!("Initializing {provider} sandbox...")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ cached_style!(
|
|||
" {spinner:.dim} {wide_msg} {elapsed:.dim}"
|
||||
);
|
||||
cached_style!(style_header_done, " {wide_msg:.dim} {prefix:.dim}");
|
||||
cached_style!(style_header_failed, " {wide_msg:.red} {prefix:.dim}");
|
||||
cached_style!(
|
||||
style_stage_running,
|
||||
" {spinner:.cyan} {wide_msg} {elapsed:.dim}"
|
||||
|
|
|
|||
|
|
@ -437,6 +437,9 @@ impl DaytonaSandbox {
|
|||
}
|
||||
_ => {
|
||||
// Building/Pending/Pulling — fall through to poll
|
||||
self.emit(SandboxEvent::SnapshotCreating {
|
||||
name: snap_cfg.name.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -457,6 +460,10 @@ impl DaytonaSandbox {
|
|||
}
|
||||
};
|
||||
|
||||
self.emit(SandboxEvent::SnapshotCreating {
|
||||
name: snap_cfg.name.clone(),
|
||||
});
|
||||
|
||||
let params = daytona_sdk::CreateSnapshotParams {
|
||||
name: snap_cfg.name.clone(),
|
||||
image: daytona_sdk::ImageSource::Custom(
|
||||
|
|
@ -627,9 +634,6 @@ impl Sandbox for DaytonaSandbox {
|
|||
let init_start = Instant::now();
|
||||
|
||||
let params = if let Some(ref snap_cfg) = self.config.snapshot {
|
||||
self.emit(SandboxEvent::SnapshotEnsuring {
|
||||
name: snap_cfg.name.clone(),
|
||||
});
|
||||
let snap_start = Instant::now();
|
||||
if let Err(e) = self.ensure_snapshot(snap_cfg).await {
|
||||
self.emit(SandboxEvent::SnapshotFailed {
|
||||
|
|
|
|||
|
|
@ -93,6 +93,12 @@ pub struct DockerSandbox {
|
|||
event_callback: Option<SandboxEventCallback>,
|
||||
}
|
||||
|
||||
enum EnsureImageOutcome {
|
||||
Skipped,
|
||||
AlreadyLocal,
|
||||
Pulled,
|
||||
}
|
||||
|
||||
impl DockerSandbox {
|
||||
pub fn new(
|
||||
config: DockerSandboxOptions,
|
||||
|
|
@ -441,13 +447,13 @@ impl DockerSandbox {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn ensure_image(&self) -> crate::Result<()> {
|
||||
async fn ensure_image(&self) -> crate::Result<EnsureImageOutcome> {
|
||||
if !self.config.auto_pull {
|
||||
return Ok(());
|
||||
return Ok(EnsureImageOutcome::Skipped);
|
||||
}
|
||||
|
||||
match self.docker.inspect_image(&self.config.image).await {
|
||||
Ok(_) => return Ok(()),
|
||||
Ok(_) => return Ok(EnsureImageOutcome::AlreadyLocal),
|
||||
Err(e) if docker_not_found(&e) => {}
|
||||
Err(e) => {
|
||||
return Err(crate::Error::docker_image_inspect(
|
||||
|
|
@ -469,12 +475,15 @@ impl DockerSandbox {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
self.emit(SandboxEvent::SnapshotPulling {
|
||||
name: self.config.image.clone(),
|
||||
});
|
||||
let mut stream = self.docker.create_image(Some(opts), None, None);
|
||||
while let Some(result) = stream.next().await {
|
||||
result.map_err(|e| crate::Error::docker_image_pull(self.config.image.clone(), e))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(EnsureImageOutcome::Pulled)
|
||||
}
|
||||
|
||||
async fn create_workspace(&self) -> crate::Result<()> {
|
||||
|
|
@ -972,18 +981,26 @@ impl Sandbox for DockerSandbox {
|
|||
});
|
||||
let init_start = Instant::now();
|
||||
|
||||
self.emit(SandboxEvent::SnapshotPulling {
|
||||
name: self.config.image.clone(),
|
||||
});
|
||||
let pull_start = Instant::now();
|
||||
if let Err(e) = self.ensure_image().await {
|
||||
return Err(self.fail_init(init_start, e));
|
||||
match self.ensure_image().await {
|
||||
Ok(EnsureImageOutcome::Skipped) => {}
|
||||
Ok(EnsureImageOutcome::AlreadyLocal | EnsureImageOutcome::Pulled) => {
|
||||
let pull_duration =
|
||||
u64::try_from(pull_start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
self.emit(SandboxEvent::SnapshotReady {
|
||||
name: self.config.image.clone(),
|
||||
duration_ms: pull_duration,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
self.emit(SandboxEvent::SnapshotFailed {
|
||||
name: self.config.image.clone(),
|
||||
error: e.to_string(),
|
||||
causes: e.causes(),
|
||||
});
|
||||
return Err(self.fail_init(init_start, e));
|
||||
}
|
||||
}
|
||||
let pull_duration = u64::try_from(pull_start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
self.emit(SandboxEvent::SnapshotPulled {
|
||||
name: self.config.image.clone(),
|
||||
duration_ms: pull_duration,
|
||||
});
|
||||
|
||||
let container_name = self
|
||||
.ensure_name_available()
|
||||
|
|
|
|||
|
|
@ -254,19 +254,10 @@ pub enum SandboxEvent {
|
|||
causes: Vec<String>,
|
||||
},
|
||||
|
||||
// -- Docker --
|
||||
// -- Snapshot lifecycle --
|
||||
SnapshotPulling {
|
||||
name: String,
|
||||
},
|
||||
SnapshotPulled {
|
||||
name: String,
|
||||
duration_ms: u64,
|
||||
},
|
||||
|
||||
// -- Daytona snapshots --
|
||||
SnapshotEnsuring {
|
||||
name: String,
|
||||
},
|
||||
SnapshotCreating {
|
||||
name: String,
|
||||
},
|
||||
|
|
@ -339,12 +330,6 @@ impl SandboxEvent {
|
|||
Self::SnapshotPulling { name } => {
|
||||
debug!(name, "Snapshot pulling");
|
||||
}
|
||||
Self::SnapshotPulled { name, duration_ms } => {
|
||||
debug!(name, duration_ms, "Snapshot pulled");
|
||||
}
|
||||
Self::SnapshotEnsuring { name } => {
|
||||
debug!(name, "Snapshot ensuring");
|
||||
}
|
||||
Self::SnapshotCreating { name } => {
|
||||
debug!(name, "Snapshot creating");
|
||||
}
|
||||
|
|
@ -1168,13 +1153,6 @@ mod tests {
|
|||
SandboxEvent::SnapshotPulling {
|
||||
name: "ubuntu:22.04".into(),
|
||||
},
|
||||
SandboxEvent::SnapshotPulled {
|
||||
name: "ubuntu:22.04".into(),
|
||||
duration_ms: 5000,
|
||||
},
|
||||
SandboxEvent::SnapshotEnsuring {
|
||||
name: "my-snap".into(),
|
||||
},
|
||||
SandboxEvent::SnapshotCreating {
|
||||
name: "my-snap".into(),
|
||||
},
|
||||
|
|
@ -1202,7 +1180,7 @@ mod tests {
|
|||
},
|
||||
];
|
||||
|
||||
assert_eq!(events.len(), 15, "should test all 15 variants");
|
||||
assert_eq!(events.len(), 13, "should test all 13 variants");
|
||||
|
||||
for event in &events {
|
||||
let json = serde_json::to_string(event).unwrap();
|
||||
|
|
|
|||
|
|
@ -206,10 +206,6 @@ pub enum EventBody {
|
|||
SandboxCleanupFailed(SandboxCleanupFailedProps),
|
||||
#[serde(rename = "sandbox.snapshot.pulling")]
|
||||
SnapshotPulling(SnapshotNameProps),
|
||||
#[serde(rename = "sandbox.snapshot.pulled")]
|
||||
SnapshotPulled(SnapshotCompletedProps),
|
||||
#[serde(rename = "sandbox.snapshot.ensuring")]
|
||||
SnapshotEnsuring(SnapshotNameProps),
|
||||
#[serde(rename = "sandbox.snapshot.creating")]
|
||||
SnapshotCreating(SnapshotNameProps),
|
||||
#[serde(rename = "sandbox.snapshot.ready")]
|
||||
|
|
@ -414,8 +410,6 @@ impl EventBody {
|
|||
Self::SandboxCleanupCompleted(_) => "sandbox.cleanup.completed",
|
||||
Self::SandboxCleanupFailed(_) => "sandbox.cleanup.failed",
|
||||
Self::SnapshotPulling(_) => "sandbox.snapshot.pulling",
|
||||
Self::SnapshotPulled(_) => "sandbox.snapshot.pulled",
|
||||
Self::SnapshotEnsuring(_) => "sandbox.snapshot.ensuring",
|
||||
Self::SnapshotCreating(_) => "sandbox.snapshot.creating",
|
||||
Self::SnapshotReady(_) => "sandbox.snapshot.ready",
|
||||
Self::SnapshotFailed(_) => "sandbox.snapshot.failed",
|
||||
|
|
@ -548,8 +542,6 @@ fn is_known_event_name(event: &str) -> bool {
|
|||
| "sandbox.cleanup.completed"
|
||||
| "sandbox.cleanup.failed"
|
||||
| "sandbox.snapshot.pulling"
|
||||
| "sandbox.snapshot.pulled"
|
||||
| "sandbox.snapshot.ensuring"
|
||||
| "sandbox.snapshot.creating"
|
||||
| "sandbox.snapshot.ready"
|
||||
| "sandbox.snapshot.failed"
|
||||
|
|
@ -1242,6 +1234,34 @@ mod tests {
|
|||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retired_sandbox_snapshot_events_deserialize_as_unknown() {
|
||||
for (event_name, expected_properties) in [
|
||||
(
|
||||
"sandbox.snapshot.pulled",
|
||||
json!({"name": "buildpack-deps:noble", "duration_ms": 5000}),
|
||||
),
|
||||
("sandbox.snapshot.ensuring", json!({"name": "fabro-v8"})),
|
||||
] {
|
||||
let value = json!({
|
||||
"id": "evt_retired_snapshot",
|
||||
"ts": "2026-04-29T12:00:00.000Z",
|
||||
"run_id": fixtures::RUN_1,
|
||||
"event": event_name,
|
||||
"properties": expected_properties
|
||||
});
|
||||
|
||||
let parsed = RunEvent::from_value(value).unwrap();
|
||||
match parsed.body {
|
||||
EventBody::Unknown { name, properties } => {
|
||||
assert_eq!(name, event_name);
|
||||
assert_eq!(properties, expected_properties);
|
||||
}
|
||||
other => panic!("expected Unknown body, got {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_snapshot_failed_omits_empty_optional_fields() {
|
||||
let body = EventBody::MetadataSnapshotFailed(MetadataSnapshotFailedProps {
|
||||
|
|
|
|||
|
|
@ -769,15 +769,6 @@ fn event_body_from_event(event: &Event) -> EventBody {
|
|||
SandboxEvent::SnapshotPulling { name } => {
|
||||
EventBody::SnapshotPulling(fabro_types::SnapshotNameProps { name: name.clone() })
|
||||
}
|
||||
SandboxEvent::SnapshotPulled { name, duration_ms } => {
|
||||
EventBody::SnapshotPulled(fabro_types::SnapshotCompletedProps {
|
||||
name: name.clone(),
|
||||
duration_ms: *duration_ms,
|
||||
})
|
||||
}
|
||||
SandboxEvent::SnapshotEnsuring { name } => {
|
||||
EventBody::SnapshotEnsuring(fabro_types::SnapshotNameProps { name: name.clone() })
|
||||
}
|
||||
SandboxEvent::SnapshotCreating { name } => {
|
||||
EventBody::SnapshotCreating(fabro_types::SnapshotNameProps { name: name.clone() })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,8 +92,6 @@ pub fn event_name(event: &Event) -> &'static str {
|
|||
SandboxEvent::CleanupCompleted { .. } => "sandbox.cleanup.completed",
|
||||
SandboxEvent::CleanupFailed { .. } => "sandbox.cleanup.failed",
|
||||
SandboxEvent::SnapshotPulling { .. } => "sandbox.snapshot.pulling",
|
||||
SandboxEvent::SnapshotPulled { .. } => "sandbox.snapshot.pulled",
|
||||
SandboxEvent::SnapshotEnsuring { .. } => "sandbox.snapshot.ensuring",
|
||||
SandboxEvent::SnapshotCreating { .. } => "sandbox.snapshot.creating",
|
||||
SandboxEvent::SnapshotReady { .. } => "sandbox.snapshot.ready",
|
||||
SandboxEvent::SnapshotFailed { .. } => "sandbox.snapshot.failed",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue