fabro/lib/apps/fabro-cli/tests/it/workflow/docker.rs
Scott Werner a1a98c69d0 fix: address review of the in-process sandbox providers
- Keep plugin-era Daytona lease fingerprints: read only DAYTONA_API_URL and
  DAYTONA_ORGANIZATION_ID (no URL alias, no placement target), and stop
  forwarding DAYTONA_SERVER_URL and DAYTONA_TARGET to the worker.
- Take the Docker fingerprint and network from this process's DOCKER_HOST,
  the endpoint the Docker client actually connects to; make the provider
  configuration's fields private.
- Return an error instead of panicking when Petri supplies no Host registry.
- Run deletion reads the Daytona key only for a Daytona run, and a forced
  or restarted delete goes on when the secret store fails, as it does for
  every other prune failure.
- Stop putting DAYTONA_API_KEY in the worker's environment; the worker reads
  it from the vault. Give the worker's Daytona client the shared HTTP client.
- Fork, rewind and retry no longer read the vault: a fork acquires no sandbox.
- Remove the dead worker plugin forwarding and document that runs execute
  only on the built-in providers.
- Build every Petri runtime through providers::standard_runtime or
  bare_runtime, with a Clippy lint against Runtime::standard/bare.
- Share the Docker require-or-skip policy in fabro-test, tighten the Host
  scope assertion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 12:31:37 -04:00

117 lines
4.1 KiB
Rust

//! The Docker provider for the workflow scenarios: an environment on
//! [`DOCKER_IMAGE`], on an isolated server.
//!
//! Petri uses the built-in Docker provider; no plugin executable is needed.
//! A scenario configured here runs against its own server so the environment
//! it creates never leaks into the shared session server.
#![expect(
clippy::disallowed_methods,
reason = "a failed setup reads the isolated server's log synchronously"
)]
#![expect(
clippy::print_stderr,
reason = "a failed setup prints the server log tail on the test's stderr"
)]
use std::path::Path;
use fabro_test::{TestContext, expect_reqwest_status};
use serde_json::json;
use crate::cmd::support::server_endpoint;
const DOCKER_IMAGE: &str = "buildpack-deps:noble";
/// The environment id the scenario selects with `--environment`.
pub(crate) const ENVIRONMENT: &str = "docker";
/// Point `context` at an isolated server with a Docker environment on
/// [`DOCKER_IMAGE`]. Returns the environment id, or `None` when the
/// prerequisites are missing and the test should skip.
pub(crate) fn configure(context: &mut TestContext) -> Option<&'static str> {
if !fabro_test::docker_image_available(DOCKER_IMAGE) {
return None;
}
let storage_dir = context.temp_dir.join("docker-server-storage");
let settings = format!(
r#"[server.storage]
root = "{storage}"
[server.auth]
methods = ["dev-token"]
"#,
storage = toml_path(&storage_dir),
);
context.write_home(".fabro/settings.toml", settings);
context.isolated_server();
create_environment(&context.storage_dir);
Some(ENVIRONMENT)
}
fn toml_path(path: &Path) -> String {
path.display().to_string().replace('\\', "/")
}
fn create_environment(storage_dir: &Path) {
let body = json!({
"id": ENVIRONMENT,
"provider": "docker",
"image": { "docker": DOCKER_IMAGE, "dockerfile": null },
"resources": { "cpu": null, "memory": null, "disk": null },
"network": { "mode": "allow_all", "allow": [] },
"lifecycle": { "preserve": false, "stop_on_terminal": true, "auto_stop": null },
"labels": {},
"env": {}
});
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("test runtime should build")
.block_on(async {
let (client, base_url) =
server_endpoint(storage_dir).expect("isolated server endpoint should exist");
let response = client
.post(format!("{base_url}/api/v1/environments"))
.json(&body)
.send()
.await
.expect("environment create request should send");
if response.status() != fabro_http::StatusCode::CREATED {
eprintln!("server log tail:\n{}", server_log_tail(storage_dir));
}
expect_reqwest_status(
response,
fabro_http::StatusCode::CREATED,
"POST /api/v1/environments",
)
.await;
});
}
/// Run a scenario; when it fails, print the isolated server's log first, since
/// the worker's stderr (and so a sandbox provider failure) lands only there
/// and the server root is removed when the context drops.
pub(crate) fn run_with_server_log(context: &TestContext, scenario: impl FnOnce()) {
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(scenario));
if let Err(panic) = outcome {
eprintln!(
"server log tail:\n{}",
server_log_tail(&context.storage_dir)
);
std::panic::resume_unwind(panic);
}
}
/// The last lines of the isolated server's log, for a failure message.
pub(crate) fn server_log_tail(storage_dir: &Path) -> String {
let path = fabro_config::Storage::new(storage_dir)
.runtime_directory()
.log_path();
let Ok(contents) = std::fs::read_to_string(&path) else {
return format!("(no server log at {})", path.display());
};
let lines: Vec<&str> = contents.lines().collect();
let start = lines.len().saturating_sub(60);
lines[start..].join("\n")
}