feat(worker): separate restored candidates, repair, and activation
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
//! Fail-closed preflight shared by restore fixtures across Worker, Runtime and Server.
|
||||
//!
|
||||
//! The normal runner supplies a separate namespace per process; a manually
|
||||
//! configured invocation may use the checkout-owned base itself. This module
|
||||
//! never sets process-wide environment or creates directories/tables.
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
fn validate_restore_test_paths(
|
||||
base: &Path,
|
||||
resolved: [Option<PathBuf>; 5],
|
||||
) -> Result<PathBuf, &'static str> {
|
||||
let root = resolved[0]
|
||||
.as_deref()
|
||||
.filter(|config| config.file_name().is_some_and(|name| name == "config"))
|
||||
.and_then(Path::parent)
|
||||
.ok_or("config")?;
|
||||
let namespace = root.strip_prefix(base).map_err(|_| "config")?;
|
||||
if namespace
|
||||
.components()
|
||||
.any(|part| !matches!(part, Component::Normal(_)))
|
||||
{
|
||||
return Err("config");
|
||||
}
|
||||
for ((label, suffix), actual) in [
|
||||
("config", "config"),
|
||||
("data", "data"),
|
||||
("secrets", "data"),
|
||||
("runtime", "run"),
|
||||
("allocation", "run/workers.json"),
|
||||
]
|
||||
.into_iter()
|
||||
.zip(resolved.iter())
|
||||
{
|
||||
if actual.as_deref() != Some(root.join(suffix).as_path()) {
|
||||
return Err(label);
|
||||
}
|
||||
}
|
||||
Ok(root.to_path_buf())
|
||||
}
|
||||
|
||||
fn require_owned_directory(checkout: &Path, directory: &Path) {
|
||||
let mut checked = checkout.to_path_buf();
|
||||
for part in directory.strip_prefix(checkout).unwrap().components() {
|
||||
assert!(matches!(part, Component::Normal(_)));
|
||||
checked.push(part);
|
||||
let metadata = fs::symlink_metadata(&checked)
|
||||
.expect("create isolated restore test directories before launching the test process");
|
||||
assert!(
|
||||
metadata.is_dir() && !metadata.file_type().is_symlink(),
|
||||
"restore test directory must not traverse symlinks"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn require_restore_test_environment() {
|
||||
let checkout = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap();
|
||||
let base = checkout.join("target/restore-test-environment");
|
||||
// Compare all resolver results before inspecting even an absent allocation
|
||||
// table (its lookup can create a lock). Never inspect a live fallback path.
|
||||
let root = validate_restore_test_paths(
|
||||
&base,
|
||||
[
|
||||
manifest::paths::config_dir(),
|
||||
manifest::paths::data_dir(),
|
||||
manifest::paths::secret_data_dir(),
|
||||
manifest::paths::runtime_dir(),
|
||||
manifest::paths::worker_allocation_path(),
|
||||
],
|
||||
)
|
||||
.unwrap_or_else(|label| {
|
||||
panic!(
|
||||
"restore fixture requires isolated {label}: run deno task test, or configure YOI_CONFIG_DIR=<checkout>/target/restore-test-environment/config, YOI_DATA_DIR=<checkout>/target/restore-test-environment/data, YOI_RUNTIME_DIR=<checkout>/target/restore-test-environment/run before launch"
|
||||
)
|
||||
});
|
||||
assert_eq!(fs::canonicalize(checkout).unwrap(), checkout);
|
||||
for directory in [
|
||||
root.clone(),
|
||||
root.join("config"),
|
||||
root.join("data"),
|
||||
root.join("run"),
|
||||
] {
|
||||
require_owned_directory(checkout, &directory);
|
||||
}
|
||||
for name in ["workers.json", "workers.json.lock"] {
|
||||
match fs::symlink_metadata(root.join("run").join(name)) {
|
||||
Ok(metadata) => assert!(
|
||||
metadata.is_file() && !metadata.file_type().is_symlink(),
|
||||
"owned allocation paths must be regular files, not symlinks"
|
||||
),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(error) => panic!("inspect owned restore allocation path: {error}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restore_fixture_path_validation_rejects_fallback_before_resource_access() {
|
||||
let base = Path::new("/owned/test-environment");
|
||||
for root in [base.to_path_buf(), base.join("run-1/worker-shard-2")] {
|
||||
let safe = ["config", "data", "data", "run", "run/workers.json"]
|
||||
.map(|suffix| Some(root.join(suffix)));
|
||||
assert_eq!(
|
||||
validate_restore_test_paths(base, safe.clone()),
|
||||
Ok(root.clone())
|
||||
);
|
||||
for index in 0..safe.len() {
|
||||
for fallback in [None, Some(PathBuf::from("/not-a-fixture"))] {
|
||||
let mut unsafe_paths = safe.clone();
|
||||
unsafe_paths[index] = fallback;
|
||||
assert!(validate_restore_test_paths(base, unsafe_paths).is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
for root in [
|
||||
base.join("../escape"),
|
||||
base.join("safe/../../escape"),
|
||||
PathBuf::from("/outside"),
|
||||
] {
|
||||
let paths = ["config", "data", "data", "run", "run/workers.json"]
|
||||
.map(|suffix| Some(root.join(suffix)));
|
||||
assert!(validate_restore_test_paths(base, paths).is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user