Run the workflow scenarios on the Docker provider instead of the stdio plugins

The host_plugin_ and docker_plugin_ variants ran each scenario under
Fabro's old plugin transport with the provider kinds `host` and
`docker-plugin`, which Petri's Fabro frontend rejects. Under Petri every
provider is already served by a sandbox-driver plugin, so those variants
test nothing distinct. A single docker_ variant replaces them: an
environment with provider `docker` on buildpack-deps:noble, created on
an isolated server, skipping without the sandbox-driver-docker
executable or a daemon with the image unless
FABRO_REQUIRE_SANDBOX_PLUGINS is set.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-09-19 18:20:24 -04:00
parent 49a647e3a4
commit beac547a1f
No known key found for this signature in database
2 changed files with 46 additions and 130 deletions

View file

@ -1,15 +1,13 @@
//! Sandbox providers served by sandbox-driver plugin executables, for the
//! workflow scenarios.
//! The Docker provider for the workflow scenarios: an environment on
//! [`DOCKER_IMAGE`], on an isolated server.
//!
//! The executables are the driver's own `sandbox-driver-host` and
//! `sandbox-driver-docker`, found on `PATH`; CI installs them at the rev the
//! workspace pins, and a developer installs them with
//! Petri serves every provider through a sandbox-driver plugin executable it
//! finds on `PATH` (`sandbox-driver-docker` here); CI installs the
//! executables at the rev the workspace pins, and a developer installs them
//! with
//! `cargo install --locked --git https://github.com/lithoscomputer/sandbox-driver --rev <rev> sandbox-driver-host sandbox-driver-docker`.
//! Each runs under a kind of the scenario's choosing (`host`,
//! `docker-plugin`): the configured kind names the plugin, whatever the
//! executable declares. A scenario configured here runs against its own
//! server so the plugin settings and the environment it creates never leak
//! into the shared session server.
//! 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,
@ -32,59 +30,27 @@ use crate::cmd::support::server_endpoint;
/// skipping it.
const REQUIRE_ENV: &str = "FABRO_REQUIRE_SANDBOX_PLUGINS";
const DOCKER_IMAGE: &str = "buildpack-deps:noble";
const DOCKER_PLUGIN: &str = "sandbox-driver-docker";
/// The environment id the scenario selects with `--environment`.
pub(crate) const ENVIRONMENT: &str = "docker";
#[derive(Clone, Copy, Debug)]
pub(crate) enum Plugin {
/// The driver's Host executable under the non-bundled `host` kind.
Host,
/// The driver's Docker executable under the non-bundled `docker-plugin`
/// kind: the same containers, reached over stdio.
Docker,
}
impl Plugin {
fn kind(self) -> &'static str {
match self {
Self::Host => "host",
Self::Docker => "docker-plugin",
}
}
fn executable(self) -> &'static str {
match self {
Self::Host => "sandbox-driver-host",
Self::Docker => "sandbox-driver-docker",
}
}
/// The environment id the scenario selects with `--environment`.
fn environment(self) -> &'static str {
match self {
Self::Host => "host-plugin",
Self::Docker => "docker-plugin",
}
}
}
/// Point `context` at an isolated server that serves `plugin` and has an
/// environment for it. Returns the environment id, or `None` when the
/// 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, plugin: Plugin) -> Option<&'static str> {
pub(crate) fn configure(context: &mut TestContext) -> Option<&'static str> {
let required = std::env::var_os(REQUIRE_ENV).is_some();
let Some(executable) = plugin_executable(plugin) else {
if plugin_executable().is_none() {
assert!(
!required,
"{REQUIRE_ENV} is set but the {} executable is not built",
plugin.executable()
"{REQUIRE_ENV} is set but {DOCKER_PLUGIN} is not on PATH"
);
eprintln!(
"skipping: {} is not on PATH; install the sandbox-driver executables at the rev \
Cargo.toml pins",
plugin.executable()
"skipping: {DOCKER_PLUGIN} is not on PATH; install the sandbox-driver executables at \
the rev Cargo.toml pins"
);
return None;
};
if matches!(plugin, Plugin::Docker) && !docker_image_available() {
}
if !docker_image_available() {
assert!(
!required,
"{REQUIRE_ENV} is set but no Docker daemon with {DOCKER_IMAGE} is available"
@ -93,56 +59,27 @@ pub(crate) fn configure(context: &mut TestContext, plugin: Plugin) -> Option<&'s
return None;
}
let storage_dir = context.temp_dir.join("plugin-server-storage");
let registry = context.temp_dir.join("host-registry");
std::fs::create_dir_all(&registry).expect("registry dir should be created");
let settings = match plugin {
Plugin::Host => format!(
r#"[server.storage]
let storage_dir = context.temp_dir.join("docker-server-storage");
let settings = format!(
r#"[server.storage]
root = "{storage}"
[server.auth]
methods = ["dev-token"]
[server.sandbox.providers.host]
path = "{path}"
dev = true
inherit_env = ["PATH", "HOME"]
[server.sandbox.providers.host.env]
SANDBOX_DRIVER_HOST_REGISTRY = "{registry}"
"#,
storage = toml_path(&storage_dir),
path = toml_path(&executable),
registry = toml_path(&registry),
),
Plugin::Docker => format!(
r#"[server.storage]
root = "{storage}"
[server.auth]
methods = ["dev-token"]
[server.sandbox.providers.docker-plugin]
path = "{path}"
dev = true
inherit_env = ["PATH", "HOME", "DOCKER_HOST", "DOCKER_CERT_PATH", "DOCKER_TLS_VERIFY"]
"#,
storage = toml_path(&storage_dir),
path = toml_path(&executable),
),
};
storage = toml_path(&storage_dir),
);
context.write_home(".fabro/settings.toml", settings);
context.isolated_server();
create_environment(&context.storage_dir, plugin);
Some(plugin.environment())
create_environment(&context.storage_dir);
Some(ENVIRONMENT)
}
/// The driver executable on `PATH`, when installed.
fn plugin_executable(plugin: Plugin) -> Option<PathBuf> {
/// The Docker plugin executable on `PATH`, when installed.
fn plugin_executable() -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
std::env::split_paths(&path)
.map(|dir| dir.join(plugin.executable()))
.map(|dir| dir.join(DOCKER_PLUGIN))
.find(|candidate| candidate.is_file())
}
@ -159,17 +96,11 @@ fn toml_path(path: &Path) -> String {
path.display().to_string().replace('\\', "/")
}
fn create_environment(storage_dir: &Path, plugin: Plugin) {
fn create_environment(storage_dir: &Path) {
let body = json!({
"id": plugin.environment(),
"provider": plugin.kind(),
"image": {
"docker": match plugin {
Plugin::Host => serde_json::Value::Null,
Plugin::Docker => json!(DOCKER_IMAGE),
},
"dockerfile": null
},
"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 },

View file

@ -9,11 +9,11 @@ mod command_agent_mixed;
mod command_pipeline;
mod command_routing;
mod conditional_branching;
pub(super) mod docker;
mod dry_run_examples;
mod full_stack;
mod hooks;
mod human_gate;
pub(super) mod plugin;
use std::path::{Path, PathBuf};
use std::time::Duration;
@ -173,15 +173,14 @@ fn run_stream_items(run_dir: &Path) -> Vec<RunStreamItem> {
/// Runs a scenario against every sandbox provider fabro supports:
///
/// - `local`: the bundled Host provider in-process.
/// - `daytona`: the bundled Daytona provider, live credentials required.
/// - `host-plugin`: the driver's Host executable over stdio under the
/// non-bundled `host` kind, a clone-based managed workspace.
/// - `docker-plugin`: the driver's Docker executable over stdio under the
/// non-bundled `docker-plugin` kind.
/// - `local`: the host provider, the session server's own `local` environment.
/// - `daytona`: the Daytona provider, live credentials required.
/// - `docker`: the Docker provider, an environment on `buildpack-deps:noble`
/// created on an isolated server.
///
/// The plugin variants need the driver's executables on `PATH`; without
/// them (or without a Docker daemon) they skip,
/// Petri serves each provider through the matching sandbox-driver plugin
/// executable on `PATH`. The `docker` variant skips without
/// `sandbox-driver-docker` or without a Docker daemon that has the image,
/// unless `FABRO_REQUIRE_SANDBOX_PLUGINS` is set, as CI sets it.
macro_rules! sandbox_tests {
($name:ident) => {
@ -200,24 +199,10 @@ macro_rules! sandbox_tests {
}
#[fabro_macros::e2e_test($(live($key)),*)]
fn [<host_plugin_ $name>]() {
fn [<docker_ $name>]() {
let mut context = fabro_test::test_context!();
if let Some(environment) =
$crate::workflow::plugin::configure(&mut context, $crate::workflow::plugin::Plugin::Host)
{
$crate::workflow::plugin::run_with_server_log(&context, || {
[<scenario_ $name>](&context, environment);
});
}
}
#[fabro_macros::e2e_test($(live($key)),*)]
fn [<docker_plugin_ $name>]() {
let mut context = fabro_test::test_context!();
if let Some(environment) =
$crate::workflow::plugin::configure(&mut context, $crate::workflow::plugin::Plugin::Docker)
{
$crate::workflow::plugin::run_with_server_log(&context, || {
if let Some(environment) = $crate::workflow::docker::configure(&mut context) {
$crate::workflow::docker::run_with_server_log(&context, || {
[<scenario_ $name>](&context, environment);
});
}
@ -230,7 +215,7 @@ pub(super) use sandbox_tests;
pub(super) fn timeout_for(sandbox: &str) -> Duration {
match sandbox {
"daytona" => Duration::from_mins(10),
"docker-plugin" => Duration::from_mins(5),
docker::ENVIRONMENT => Duration::from_mins(5),
_ => Duration::from_mins(3),
}
}