Remove phase vocabulary from Lazaret interfaces

This commit is contained in:
ms2k 2026-07-18 22:35:48 +09:00
commit e7dd95aea6
28 changed files with 547 additions and 579 deletions

View file

@ -0,0 +1,667 @@
use std::collections::BTreeMap;
use std::ffi::OsString;
use std::fmt;
use std::fs::File;
use std::io::{self, IsTerminal, Write};
use std::os::fd::{AsFd, AsRawFd};
use std::os::unix::fs::MetadataExt;
use std::path::PathBuf;
use std::thread;
use std::time::Duration;
use lazaret_core::configuration::{
ConfigurationLoadRequest, ResolvedDomainConfiguration, load_trusted_configuration,
};
use lazaret_host_coordinator::client_bootstrap::PRIVATE_CLIENT_BOOTSTRAP_ROLE;
use lazaret_host_coordinator::control::{ControlError, PublishedControlFace};
use lazaret_host_coordinator::waiter::{
BOOTSTRAP_WAITER_SOCKET_NAME, BootstrapWaitOutcome, BootstrapWaitRequest, read_frame,
write_frame,
};
use lazaret_linux::filesystem::try_exclusive_flock;
use lazaret_linux::platform::{PlatformObservation, check_platform};
use lazaret_linux::process::{BootstrapChild, TargetCommand, child_broker};
use lazaret_runtime_supervisor::sandbox::default_sandbox_layout;
use rustix::fs::{Mode, OFlags, openat};
use rustix::pipe::{PipeFlags, pipe_with};
use crate::{
BootstrapAcquisition, ClientBackend, ClientCommand, ClientError, ClientSessionIdentity,
ControlEndpoint, DomainStatus, execute_interactive_over_control_face_as,
execute_over_control_face_as,
};
#[derive(Debug)]
pub struct CoordinatorClientError(String);
impl fmt::Display for CoordinatorClientError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
impl std::error::Error for CoordinatorClientError {}
fn failure(error: impl fmt::Display) -> CoordinatorClientError {
CoordinatorClientError(error.to_string())
}
pub struct CoordinatorClientBackend {
configuration: Option<ResolvedDomainConfiguration>,
command: Option<ClientCommand>,
identity: ClientSessionIdentity,
coordinator: Option<BootstrapChild>,
bootstrap_owner: Option<rustix::fd::OwnedFd>,
}
impl CoordinatorClientBackend {
/// Creates one client invocation and its host-private session identity.
///
/// # Errors
/// Returns an error when operating-system randomness is unavailable.
pub fn new() -> Result<Self, ClientError<CoordinatorClientError>> {
Ok(Self {
configuration: None,
command: None,
identity: ClientSessionIdentity::generate()?,
coordinator: None,
bootstrap_owner: None,
})
}
fn configuration(&self) -> Result<&ResolvedDomainConfiguration, CoordinatorClientError> {
self.configuration
.as_ref()
.ok_or_else(|| failure("client configuration was not validated"))
}
fn discover(&self) -> Result<Discovery, CoordinatorClientError> {
let configuration = self.configuration()?;
match PublishedControlFace::discover_confined(
configuration.runtime_domain_root(),
rustix::process::getuid().as_raw(),
) {
Ok((authority, socket_path, publication))
if publication_matches(configuration, &publication) =>
{
if !control_socket_is_live(&socket_path) {
return Ok(Discovery::Stale);
}
Ok(Discovery::Ready(Box::new(ControlEndpoint::confined(
socket_path,
publication,
authority,
))))
}
Err(ControlError::Io(error)) if error.kind() == io::ErrorKind::NotFound => {
Ok(Discovery::Absent)
}
Ok(_) | Err(_) => Ok(Discovery::Stale),
}
}
fn domain_lock_is_available(&self) -> Result<bool, CoordinatorClientError> {
let configuration = self.configuration()?;
let root = configuration
.persistent_domain_root()
.open_authority()
.map_err(failure)?;
let file = open_private_domain_lock(&root)?;
match try_exclusive_flock(file) {
Ok(lock) => {
drop(lock);
Ok(true)
}
Err(error) if error.kind() == io::ErrorKind::WouldBlock => Ok(false),
Err(error) => Err(failure(error)),
}
}
fn launch_coordinator(
&mut self,
command: &ClientCommand,
) -> Result<BootstrapAcquisition, CoordinatorClientError> {
let executable = std::env::current_exe().map_err(failure)?;
let coordinator = executable
.parent()
.ok_or_else(|| failure("Lazaret executable has no installation directory"))?
.join("lazaret-host-coordinator");
let (reader, writer) = pipe_with(PipeFlags::CLOEXEC).map_err(failure)?;
let mut target = TargetCommand::new(coordinator)
.argument(PRIVATE_CLIENT_BOOTSTRAP_ROLE)
.argument("--profile")
.argument(&command.profile)
.argument("--workspace")
.argument(command.workspace.as_os_str())
.with_lifetime_pipe_parent_edge();
if let Some(configuration) = &command.configuration {
target = target
.argument("--config")
.argument(configuration.as_os_str());
}
target = target
.argument("--client-id")
.argument(hex(self.identity.cli_id().as_bytes()))
.argument("--session-id")
.argument(hex(self.identity.session_id().as_bytes()))
.argument("--anchor-id")
.argument(hex(self.identity.session_anchor_id().as_bytes()))
.argument("--domain-id")
.argument(hex(self.configuration()?.domain_id().as_bytes()))
.argument("--configuration-fingerprint")
.argument(hex(self
.configuration()?
.configuration_fingerprint()
.as_bytes()));
let mut child = child_broker()
.spawn_bootstrap(&target, File::from(reader))
.map_err(failure)?;
match child.acquisition_status().map_err(failure)? {
b'L' => {
self.coordinator = Some(child);
self.bootstrap_owner = Some(writer);
Ok(BootstrapAcquisition::Leader)
}
b'W' => {
child.reap().map_err(failure)?;
Ok(BootstrapAcquisition::Waiter)
}
b'E' => {
let result = child.reap().map_err(failure)?;
Err(failure(format!(
"Host Coordinator refused bootstrap with {result:?}"
)))
}
_ => Err(failure("Host Coordinator sent an invalid bootstrap status")),
}
}
fn await_publication(
&mut self,
) -> Result<ControlEndpoint, ClientError<CoordinatorClientError>> {
loop {
match self.discover().map_err(ClientError::Backend)? {
Discovery::Ready(endpoint) => return Ok(*endpoint),
Discovery::Stale | Discovery::Absent => {}
}
if self
.coordinator
.as_ref()
.ok_or_else(|| ClientError::Backend(failure("Host Coordinator is absent")))?
.has_exited()
.map_err(failure)
.map_err(ClientError::Backend)?
{
let status = self
.coordinator
.take()
.expect("checked coordinator remains present")
.reap()
.map_err(failure)
.map_err(ClientError::Backend)?;
return Err(ClientError::Backend(failure(format!(
"Host Coordinator exited before publication with {status:?}"
))));
}
thread::sleep(Duration::from_millis(10));
}
}
}
impl ClientBackend for CoordinatorClientBackend {
type Endpoint = ControlEndpoint;
type Error = CoordinatorClientError;
fn validate(&mut self, command: &ClientCommand) -> Result<(), ClientError<Self::Error>> {
let kernel_release = std::fs::read_to_string("/proc/sys/kernel/osrelease")
.map_err(|error| ClientError::UnsupportedHost(error.to_string()))?;
check_platform(&PlatformObservation {
operating_system: std::env::consts::OS,
architecture: std::env::consts::ARCH,
kernel_release: kernel_release.trim(),
})
.map_err(|error| ClientError::UnsupportedHost(error.to_string()))?;
self.configuration = Some(
load_configuration(command)
.map_err(|error| ClientError::Configuration(error.to_string()))?,
);
self.command = Some(command.clone());
Ok(())
}
fn observe(&mut self) -> Result<DomainStatus<Self::Endpoint>, Self::Error> {
match self.discover()? {
Discovery::Ready(endpoint) => return Ok(DomainStatus::Ready(*endpoint)),
Discovery::Stale => return Ok(DomainStatus::Stale),
Discovery::Absent => {}
}
Ok(DomainStatus::Absent)
}
fn acquire_bootstrap(&mut self) -> Result<BootstrapAcquisition, Self::Error> {
let command = self
.command
.clone()
.ok_or_else(|| failure("client command was not validated"))?;
self.launch_coordinator(&command)
}
fn bootstrap(&mut self) -> Result<Self::Endpoint, ClientError<Self::Error>> {
self.await_publication()
}
fn wait_until_ready(&mut self) -> Result<Self::Endpoint, ClientError<Self::Error>> {
let configuration = self.configuration().map_err(ClientError::Backend)?;
let runtime_authority = configuration
.runtime_domain_root()
.open_authority()
.map_err(failure)
.map_err(ClientError::Backend)?;
let socket = PathBuf::from(format!(
"/proc/self/fd/{}/{}",
runtime_authority.as_raw_fd(),
BOOTSTRAP_WAITER_SOCKET_NAME
));
let request = BootstrapWaitRequest {
protocol_version: lazaret_core::records::ProtocolVersion::V1,
domain_id: configuration.domain_id(),
configuration_fingerprint: configuration.configuration_fingerprint(),
cli_id: self.identity.cli_id(),
session_id: self.identity.session_id(),
session_anchor_id: self.identity.session_anchor_id(),
};
loop {
match self.discover().map_err(ClientError::Backend)? {
Discovery::Ready(endpoint) => return Ok(*endpoint),
Discovery::Stale | Discovery::Absent => {}
}
match std::os::unix::net::UnixStream::connect(&socket) {
Ok(mut stream) => {
write_frame(&mut stream, &request).map_err(|error| {
ClientError::Backend(failure(format!(
"bootstrap waiter request failed: {error}"
)))
})?;
let first: BootstrapWaitOutcome = read_frame(&mut stream).map_err(|error| {
ClientError::Backend(failure(format!(
"bootstrap waiter admission failed: {error}"
)))
})?;
if first != BootstrapWaitOutcome::Waiting {
return match first {
BootstrapWaitOutcome::Refused => Err(ClientError::StaleCoordinator),
BootstrapWaitOutcome::Failed => Err(ClientError::WaitingDomainFailed),
BootstrapWaitOutcome::Published => {
self.await_ready_publication(Some(stream))
}
BootstrapWaitOutcome::Waiting => unreachable!(),
};
}
return match read_frame(&mut stream) {
Ok(BootstrapWaitOutcome::Published) => {
self.await_ready_publication(Some(stream))
}
Ok(BootstrapWaitOutcome::Failed) | Err(_) => {
Err(ClientError::WaitingDomainFailed)
}
Ok(BootstrapWaitOutcome::Waiting | BootstrapWaitOutcome::Refused) => {
Err(ClientError::Protocol(
"bootstrap waiter sent an invalid terminal outcome",
))
}
};
}
Err(error)
if matches!(
error.kind(),
io::ErrorKind::NotFound | io::ErrorKind::ConnectionRefused
) => {}
Err(error) => return Err(ClientError::Backend(failure(error))),
}
if self
.domain_lock_is_available()
.map_err(ClientError::Backend)?
{
return Err(ClientError::WaitingDomainFailed);
}
thread::sleep(Duration::from_millis(10));
}
}
fn execute(
&mut self,
endpoint: Self::Endpoint,
argv: &[Vec<u8>],
stdout: &mut dyn Write,
stderr: &mut dyn Write,
) -> Result<i32, ClientError<Self::Error>> {
let exit = if std::io::stdin().is_terminal() && std::io::stdout().is_terminal() {
let mut terminal = crate::terminal::SystemTerminal::open()?;
execute_interactive_over_control_face_as(
&endpoint,
argv,
&mut terminal,
&self.identity,
)?
} else {
execute_over_control_face_as(&endpoint, argv, stdout, stderr, &self.identity)?
};
if let Some(coordinator) = self.coordinator.take() {
let status = coordinator
.reap()
.map_err(failure)
.map_err(ClientError::Backend)?;
if status.exit_code != Some(0) || status.terminating_signal.is_some() {
return Err(ClientError::Backend(failure(format!(
"Host Coordinator teardown failed with {status:?}"
))));
}
}
self.bootstrap_owner.take();
Ok(exit)
}
}
impl CoordinatorClientBackend {
fn await_ready_publication(
&self,
waiter: Option<std::os::unix::net::UnixStream>,
) -> Result<ControlEndpoint, ClientError<CoordinatorClientError>> {
loop {
match self.discover().map_err(ClientError::Backend)? {
Discovery::Ready(endpoint) => {
let endpoint = *endpoint;
return Ok(match waiter {
Some(stream) => endpoint.retain_waiter_transfer(stream),
None => endpoint,
});
}
Discovery::Stale => return Err(ClientError::StaleCoordinator),
Discovery::Absent => thread::sleep(Duration::from_millis(1)),
}
}
}
}
pub(crate) fn open_private_domain_lock(root: &impl AsFd) -> Result<File, CoordinatorClientError> {
let file = File::from(
openat(
root,
"domain.lock",
OFlags::RDWR | OFlags::CREATE | OFlags::CLOEXEC | OFlags::NOFOLLOW,
Mode::RUSR | Mode::WUSR,
)
.map_err(failure)?,
);
let metadata = file.metadata().map_err(failure)?;
if !metadata.file_type().is_file()
|| metadata.uid() != rustix::process::getuid().as_raw()
|| metadata.mode() & 0o7777 != 0o600
|| metadata.nlink() != 1
{
return Err(failure(
"Domain lock is not exact invoking-user mode-0600 authority",
));
}
Ok(file)
}
fn control_socket_is_live(socket_path: &std::path::Path) -> bool {
std::os::unix::net::UnixStream::connect(socket_path).is_ok()
}
fn publication_matches(
configuration: &ResolvedDomainConfiguration,
publication: &lazaret_core::records::ProtocolPublicationIdentity,
) -> bool {
publication.generation_identity.domain_id == configuration.domain_id()
&& publication.generation_identity.configuration_fingerprint
== configuration.configuration_fingerprint()
}
pub(crate) fn load_configuration(
command: &ClientCommand,
) -> Result<ResolvedDomainConfiguration, Box<dyn std::error::Error>> {
Ok(load_trusted_configuration(ConfigurationLoadRequest {
explicit_configuration_path: command.configuration.clone(),
selected_profile: command.profile.clone(),
workspace: command.workspace.clone(),
home: required_environment_path("HOME")?,
xdg_config_home: optional_environment_path("XDG_CONFIG_HOME"),
xdg_data_home: optional_environment_path("XDG_DATA_HOME"),
xdg_runtime_dir: required_environment_path("XDG_RUNTIME_DIR")?,
environment: environment(),
sandbox_layout: default_sandbox_layout()?,
})?)
}
fn required_environment_path(name: &str) -> Result<PathBuf, CoordinatorClientError> {
std::env::var_os(name)
.filter(|value| !value.is_empty())
.map(PathBuf::from)
.ok_or_else(|| failure(format!("{name} is not set")))
}
fn optional_environment_path(name: &str) -> Option<PathBuf> {
std::env::var_os(name)
.filter(|value| !value.is_empty())
.map(PathBuf::from)
}
fn environment() -> BTreeMap<String, OsString> {
std::env::vars_os()
.filter_map(|(name, value)| name.into_string().ok().map(|name| (name, value)))
.collect()
}
fn hex(bytes: &[u8]) -> String {
use fmt::Write as _;
bytes.iter().fold(String::new(), |mut output, byte| {
write!(output, "{byte:02x}").expect("writing to a String cannot fail");
output
})
}
enum Discovery {
Absent,
Ready(Box<ControlEndpoint>),
Stale,
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use std::fs;
use std::io::Read;
use std::os::unix::ffi::OsStringExt;
use std::os::unix::fs::PermissionsExt;
use std::os::unix::net::{UnixListener, UnixStream};
use super::*;
use lazaret_core::codec::encode;
use lazaret_core::identity::{
CertificateDigest, CoordinatorId, GenerationId, SandboxId, TrustManifestDigest,
};
use lazaret_core::records::{GenerationIdentity, ProtocolPublicationIdentity, ProtocolVersion};
#[test]
fn replaced_domain_roots_cannot_redirect_client_lock_or_control_authority() {
let persistent_tree = tempfile::Builder::new()
.prefix("lz-")
.tempdir_in("/tmp")
.unwrap();
let persistent_configuration = configuration(persistent_tree.path());
let persistent_path = path(
persistent_configuration
.persistent_domain_root()
.canonical_label()
.as_bytes(),
);
fs::rename(&persistent_path, persistent_path.with_extension("old")).unwrap();
fs::create_dir(&persistent_path).unwrap();
fs::set_permissions(&persistent_path, fs::Permissions::from_mode(0o700)).unwrap();
assert!(
persistent_configuration
.persistent_domain_root()
.open_authority()
.is_err()
);
assert!(!persistent_path.join("domain.lock").exists());
let runtime_tree = tempfile::Builder::new()
.prefix("lz-")
.tempdir_in("/tmp")
.unwrap();
let runtime_configuration = configuration(runtime_tree.path());
let runtime_path = path(
runtime_configuration
.runtime_domain_root()
.canonical_label()
.as_bytes(),
);
fs::rename(&runtime_path, runtime_path.with_extension("old")).unwrap();
fs::create_dir(&runtime_path).unwrap();
fs::set_permissions(&runtime_path, fs::Permissions::from_mode(0o700)).unwrap();
assert!(
PublishedControlFace::discover_confined(
runtime_configuration.runtime_domain_root(),
rustix::process::getuid().as_raw(),
)
.is_err()
);
}
#[test]
fn confined_control_discovery_connects_through_the_retained_root() {
let tree = tempfile::Builder::new()
.prefix("lz-")
.tempdir_in("/tmp")
.unwrap();
let configuration = configuration(tree.path());
let runtime_path = path(
configuration
.runtime_domain_root()
.canonical_label()
.as_bytes(),
);
let listener = UnixListener::bind(runtime_path.join("c.sock")).unwrap();
fs::set_permissions(
runtime_path.join("c.sock"),
fs::Permissions::from_mode(0o600),
)
.unwrap();
let publication = ProtocolPublicationIdentity {
protocol_version: ProtocolVersion::V1,
generation_identity: GenerationIdentity {
agent_profile: "agent".into(),
domain_id: configuration.domain_id(),
generation_id: GenerationId::new(1),
configuration_fingerprint: configuration.configuration_fingerprint(),
coordinator_id: CoordinatorId::new([3; 32]),
sandbox_id: SandboxId::new([4; 32]),
},
tls_ca_certificate_der: vec![1],
trust_manifest_sha256: TrustManifestDigest::new([5; 32]),
coordinator_tls_certificate_sha256: CertificateDigest::new([6; 32]),
};
let mut mismatch = publication.clone();
mismatch.generation_identity.configuration_fingerprint =
lazaret_core::identity::ConfigurationFingerprint::new([7; 32]);
assert!(publication_matches(&configuration, &publication));
assert!(!publication_matches(&configuration, &mismatch));
fs::write(
runtime_path.join("protocol-identity.postcard"),
encode(&publication).unwrap(),
)
.unwrap();
fs::set_permissions(
runtime_path.join("protocol-identity.postcard"),
fs::Permissions::from_mode(0o600),
)
.unwrap();
let (authority, socket, observed) = PublishedControlFace::discover_confined(
configuration.runtime_domain_root(),
rustix::process::getuid().as_raw(),
)
.unwrap();
let client = UnixStream::connect(socket).unwrap();
let (server, _) = listener.accept().unwrap();
assert_eq!(observed, publication);
assert!(control_socket_is_live(&runtime_path.join("c.sock")));
let (probe, _) = listener.accept().unwrap();
drop((authority, client, server, probe, listener));
assert!(!control_socket_is_live(&runtime_path.join("c.sock")));
}
#[test]
fn waiter_transfer_remains_live_until_control_registration_acknowledges_it() {
let (client, mut coordinator) = UnixStream::pair().unwrap();
coordinator.set_nonblocking(true).unwrap();
let endpoint = ControlEndpoint::new(
PathBuf::from("unused"),
ProtocolPublicationIdentity {
protocol_version: ProtocolVersion::V1,
generation_identity: GenerationIdentity {
agent_profile: "agent".into(),
domain_id: lazaret_core::identity::DomainId::new([1; 32]),
generation_id: GenerationId::new(1),
configuration_fingerprint:
lazaret_core::identity::ConfigurationFingerprint::new([2; 32]),
coordinator_id: CoordinatorId::new([3; 32]),
sandbox_id: SandboxId::new([4; 32]),
},
tls_ca_certificate_der: vec![1],
trust_manifest_sha256: TrustManifestDigest::new([5; 32]),
coordinator_tls_certificate_sha256: CertificateDigest::new([6; 32]),
},
)
.retain_waiter_transfer(client);
assert_eq!(
coordinator.read(&mut [0_u8; 1]).unwrap_err().kind(),
io::ErrorKind::WouldBlock
);
endpoint
.acknowledge_waiter_transfer::<std::convert::Infallible>()
.unwrap();
assert_eq!(coordinator.read(&mut [0_u8; 1]).unwrap(), 0);
}
fn configuration(root: &std::path::Path) -> ResolvedDomainConfiguration {
let configuration_directory = root.join("configuration");
let workspace = root.join("workspace");
let runtime = root.join("r");
fs::create_dir(&configuration_directory).unwrap();
fs::create_dir(&workspace).unwrap();
fs::create_dir(&runtime).unwrap();
let configuration_path = configuration_directory.join("lazaret.toml");
fs::write(
&configuration_path,
format!(
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n[profiles.agent]\nimage = \"example.invalid/image:v1\"\n[profiles.agent.readiness]\ndenied = [{{ address = \"10.0.0.1\", port = 443 }}]\npermitted = [{{ address = \"192.0.2.10\", port = 443 }}]\ndns = [{{ resolver = \"198.51.100.53\", query = \"example.invalid\" }}]\n",
root.join("data").display()
),
)
.unwrap();
load_trusted_configuration(ConfigurationLoadRequest {
explicit_configuration_path: Some(configuration_path),
selected_profile: "agent".into(),
workspace,
home: root.join("home"),
xdg_config_home: None,
xdg_data_home: None,
xdg_runtime_dir: runtime,
environment: BTreeMap::new(),
sandbox_layout: default_sandbox_layout().unwrap(),
})
.unwrap()
}
fn path(bytes: &[u8]) -> PathBuf {
PathBuf::from(OsString::from_vec(bytes.to_vec()))
}
}

View file

@ -22,7 +22,7 @@ use sha2::{Digest, Sha256};
use crate::terminal::{HostTerminal, InteractiveSessionTransport, run_interactive_session};
pub mod production;
pub mod coordinator_backend;
pub mod state;
pub mod terminal;

View file

@ -30,7 +30,7 @@ fn main() {
std::process::exit(2);
}
};
let mut backend = match lazaret_client::production::ProductionClientBackend::new() {
let mut backend = match lazaret_client::coordinator_backend::CoordinatorClientBackend::new() {
Ok(backend) => backend,
Err(error) => {
eprintln!("Lazaret failed: {error}");

View file

@ -14,7 +14,7 @@ use rustix::fs::{Mode, OFlags, ResolveFlags, openat2};
use serde_json::json;
use crate::ClientCommand;
use crate::production::{load_configuration, open_private_domain_lock};
use crate::coordinator_backend::{load_configuration, open_private_domain_lock};
const OUTPUT_SCHEMA: &str = "lazaret-state-output-v1";

View file

@ -128,12 +128,12 @@ impl fmt::Debug for SandboxLayoutV1 {
}
impl SandboxLayoutV1 {
/// Builds the closed destination set supplied by production sandbox-layout construction.
/// Builds the closed destination set supplied by default sandbox-layout construction.
///
/// # Errors
/// Every destination must be a normalized absolute non-root sandbox path and the set must
/// include `/workspace`.
pub fn from_production_layout(
pub fn from_protected_destinations(
destinations: impl IntoIterator<Item = String>,
) -> Result<Self, ConfigurationError> {
let mut protected_destinations = BTreeSet::new();

View file

@ -1,4 +1,4 @@
#![doc = "Shared production interfaces for Lazaret processes."]
#![doc = "Shared interfaces for Lazaret processes."]
pub mod codec;
pub mod configuration;

View file

@ -58,7 +58,7 @@ fn write_configuration(tree: &TestTree, body: &str) -> PathBuf {
}
fn layout() -> SandboxLayoutV1 {
SandboxLayoutV1::from_production_layout([
SandboxLayoutV1::from_protected_destinations([
"/workspace".to_owned(),
"/proc".to_owned(),
"/sys".to_owned(),
@ -187,14 +187,14 @@ volumes = [{ host = "/source", container = "/root/source", kind = "socket" }]
#[test]
fn sandbox_layout_is_closed_normalized_and_requires_workspace() {
assert!(SandboxLayoutV1::from_production_layout(["/proc".into()]).is_err());
assert!(SandboxLayoutV1::from_protected_destinations(["/proc".into()]).is_err());
assert!(
SandboxLayoutV1::from_production_layout(["/workspace".into(), "/workspace".into()])
SandboxLayoutV1::from_protected_destinations(["/workspace".into(), "/workspace".into()])
.is_err()
);
for invalid in ["workspace", "/", "/workspace/", "/workspace/../control"] {
assert!(
SandboxLayoutV1::from_production_layout(["/workspace".into(), invalid.into(),])
SandboxLayoutV1::from_protected_destinations(["/workspace".into(), invalid.into(),])
.is_err()
);
}

View file

@ -49,8 +49,8 @@ path = "tests/escalation.rs"
harness = false
[[test]]
name = "production"
path = "tests/production.rs"
name = "cold-start"
path = "tests/cold_start.rs"
[lints]
workspace = true

View file

@ -25,9 +25,7 @@ use crate::control::{
TrustAnchor,
};
use crate::domain_store::{DomainStoreMutation, HostCoordinatorDomainStore};
use crate::escalation::{
ProductionEscalation, ProductionEscalationHandle, ProductionEscalationInputs,
};
use crate::escalation::{EscalationService, EscalationServiceHandle, EscalationServiceInputs};
use crate::git_routing::GitRoutingCoordinator;
use crate::lifecycle::{
HostCoordinatorLifecycle, LifecycleAction, LifecycleAdapter, SessionAdmissionIdentity,
@ -42,27 +40,27 @@ use crate::state_anchor::{StateAnchorCoordinator, StateAnchorReleaseAuthority};
use crate::terminal::SessionPromptTransport;
#[derive(Debug)]
pub struct ProductionLifecycleError(String);
pub struct LifecycleServiceError(String);
impl fmt::Display for ProductionLifecycleError {
impl fmt::Display for LifecycleServiceError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
impl std::error::Error for ProductionLifecycleError {}
impl std::error::Error for LifecycleServiceError {}
impl ProductionLifecycleError {
impl LifecycleServiceError {
pub(crate) fn service(error: impl fmt::Display) -> Self {
Self(error.to_string())
}
}
fn failure(error: impl fmt::Display) -> ProductionLifecycleError {
ProductionLifecycleError(error.to_string())
fn failure(error: impl fmt::Display) -> LifecycleServiceError {
LifecycleServiceError(error.to_string())
}
pub struct ProductionBootstrapPlan<'a> {
pub struct BootstrapPlan<'a> {
pub store: Arc<HostCoordinatorDomainStore>,
pub catalog: &'a Catalog,
pub configuration: &'a ResolvedDomainConfiguration,
@ -81,24 +79,24 @@ pub struct ProductionBootstrapPlan<'a> {
pub non_interactive_executor: Arc<dyn NonInteractiveSessionExecutor>,
pub interactive_executor: Arc<dyn InteractiveSessionExecutor>,
pub prompt_transport: Arc<SessionPromptTransport>,
pub(crate) runtime_staging: &'a crate::production::RuntimeStaging,
pub git_routing: Option<ProductionGitRouting>,
pub(crate) runtime_staging: &'a crate::cold_start::RuntimeStaging,
pub git_routing: Option<BootstrapGitRouting>,
}
pub struct ProductionGitRouting {
pub struct BootstrapGitRouting {
pub mountpoint: HostPath,
pub host_root: HostPath,
pub isolated_root: HostPath,
}
pub struct ProductionLifecycleAdapter<'a> {
plan: ProductionBootstrapPlan<'a>,
pub struct CoordinatorLifecycleAdapter<'a> {
plan: BootstrapPlan<'a>,
registry: Arc<SessionRegistry>,
trust_anchor: Option<TrustAnchor>,
staged_control: Option<StagedControlFace>,
published_control: Option<Arc<PublishedControlFace>>,
escalation: Option<ProductionEscalation<'a>>,
escalation_handle: Option<ProductionEscalationHandle>,
escalation: Option<EscalationService<'a>>,
escalation_handle: Option<EscalationServiceHandle>,
awaiting_runtime: Option<AwaitingAttachedRunsc>,
runtime: Option<CommittedSandboxRuntime>,
state_anchor_mount: Option<MountIdentity>,
@ -107,43 +105,43 @@ pub struct ProductionLifecycleAdapter<'a> {
state_anchor_absence: Option<StateAnchorAbsenceObservation>,
}
pub type ProductionHostCoordinatorLifecycle<'store, 'resources> = HostCoordinatorLifecycle<
pub type CoordinatorLifecycle<'store, 'resources> = HostCoordinatorLifecycle<
'store,
ProductionLifecycleAdapter<'resources>,
ProductionSessionRegistry,
CoordinatorLifecycleAdapter<'resources>,
CoordinatorSessionRegistry,
>;
/// Composes the cold-start lifecycle with the exact shared production session registry.
/// Composes the cold-start lifecycle with the exact shared session registry.
///
/// # Errors
/// Refuses a plan whose Domain Store is not the caller-retained lock-owning store.
pub fn production_lifecycle<'store, 'resources>(
pub fn coordinator_lifecycle<'store, 'resources>(
store: &'store Arc<HostCoordinatorDomainStore>,
plan: ProductionBootstrapPlan<'resources>,
) -> Result<ProductionHostCoordinatorLifecycle<'store, 'resources>, ProductionLifecycleError> {
plan: BootstrapPlan<'resources>,
) -> Result<CoordinatorLifecycle<'store, 'resources>, LifecycleServiceError> {
if !Arc::ptr_eq(store, &plan.store) {
return Err(ProductionLifecycleError(
"production bootstrap Domain Store authority differs".into(),
return Err(LifecycleServiceError(
"bootstrap Domain Store authority differs".into(),
));
}
let (adapter, registry) = ProductionLifecycleAdapter::new(plan);
let (adapter, registry) = CoordinatorLifecycleAdapter::new(plan);
Ok(HostCoordinatorLifecycle::new(store, adapter, registry))
}
#[derive(Clone)]
pub struct ProductionSessionRegistry {
pub struct CoordinatorSessionRegistry {
registry: Arc<SessionRegistry>,
}
impl ProductionSessionRegistry {
impl CoordinatorSessionRegistry {
#[must_use]
pub fn registry(&self) -> Arc<SessionRegistry> {
Arc::clone(&self.registry)
}
}
impl SessionLifecycleAdapter for ProductionSessionRegistry {
type Error = ProductionLifecycleError;
impl SessionLifecycleAdapter for CoordinatorSessionRegistry {
type Error = LifecycleServiceError;
type Admission = ControlSessionAdmission;
fn admission_identity(&self, admission: &Self::Admission) -> SessionAdmissionIdentity {
@ -182,9 +180,9 @@ impl SessionLifecycleAdapter for ProductionSessionRegistry {
}
}
impl<'a> ProductionLifecycleAdapter<'a> {
impl<'a> CoordinatorLifecycleAdapter<'a> {
#[must_use]
pub fn new(plan: ProductionBootstrapPlan<'a>) -> (Self, ProductionSessionRegistry) {
pub fn new(plan: BootstrapPlan<'a>) -> (Self, CoordinatorSessionRegistry) {
let registry = Arc::new(SessionRegistry::new(
Arc::clone(&plan.store),
plan.generation_identity.clone(),
@ -205,7 +203,7 @@ impl<'a> ProductionLifecycleAdapter<'a> {
state_anchor_release: None,
state_anchor_absence: None,
},
ProductionSessionRegistry { registry },
CoordinatorSessionRegistry { registry },
)
}
@ -216,11 +214,11 @@ impl<'a> ProductionLifecycleAdapter<'a> {
pub(crate) fn take_escalation(
&mut self,
) -> Option<(ProductionEscalation<'a>, ProductionEscalationHandle)> {
) -> Option<(EscalationService<'a>, EscalationServiceHandle)> {
Some((self.escalation.take()?, self.escalation_handle.take()?))
}
fn require_generation(&self, generation: &Generation) -> Result<(), ProductionLifecycleError> {
fn require_generation(&self, generation: &Generation) -> Result<(), LifecycleServiceError> {
if generation.domain_id == self.plan.generation_identity.domain_id
&& generation.generation_id == self.plan.generation_identity.generation_id
&& generation.configuration_fingerprint
@ -230,8 +228,8 @@ impl<'a> ProductionLifecycleAdapter<'a> {
{
Ok(())
} else {
Err(ProductionLifecycleError(
"production bootstrap generation identity changed".into(),
Err(LifecycleServiceError(
"bootstrap generation identity changed".into(),
))
}
}
@ -252,18 +250,16 @@ impl<'a> ProductionLifecycleAdapter<'a> {
StateAnchorCoordinator::new(self.store(), self.plan.catalog)
}
fn create_trust_anchor(&mut self) -> Result<(), ProductionLifecycleError> {
fn create_trust_anchor(&mut self) -> Result<(), LifecycleServiceError> {
if self.trust_anchor.is_some() {
return Err(ProductionLifecycleError(
"Trust Anchor already exists".into(),
));
return Err(LifecycleServiceError("Trust Anchor already exists".into()));
}
self.trust_anchor =
Some(TrustAnchor::create(self.plan.generation_identity.clone()).map_err(failure)?);
Ok(())
}
fn start_runtime_supervisor(&mut self) -> Result<(), ProductionLifecycleError> {
fn start_runtime_supervisor(&mut self) -> Result<(), LifecycleServiceError> {
let sandbox = self.sandbox();
sandbox
.begin_intent(
@ -286,7 +282,7 @@ impl<'a> ProductionLifecycleAdapter<'a> {
Ok(())
}
fn lease_prepared_lower(&self) -> Result<(), ProductionLifecycleError> {
fn lease_prepared_lower(&self) -> Result<(), LifecycleServiceError> {
self.state_anchor()
.begin_acquisition(
self.plan.generation_identity.generation_id,
@ -297,13 +293,13 @@ impl<'a> ProductionLifecycleAdapter<'a> {
.map_err(failure)
}
fn mount_state_anchor(&mut self) -> Result<(), ProductionLifecycleError> {
fn mount_state_anchor(&mut self) -> Result<(), LifecycleServiceError> {
let store = Arc::clone(&self.plan.store);
let coordinator = StateAnchorCoordinator::new(&store, self.plan.catalog);
let mount = self
.awaiting_runtime
.as_mut()
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?
.ok_or_else(|| LifecycleServiceError("Runtime Supervisor is absent".into()))?
.mount_state_anchor(
&coordinator,
self.plan.prepared_artifact.prepared_artifact_id,
@ -323,7 +319,7 @@ impl<'a> ProductionLifecycleAdapter<'a> {
let git_mount = self
.awaiting_runtime
.as_mut()
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?
.ok_or_else(|| LifecycleServiceError("Runtime Supervisor is absent".into()))?
.start_git_routing(&git_routing)
.map_err(|error| failure(format!("starting Git routing: {error}")))?;
self.git_routing_mount = Some(git_mount);
@ -331,32 +327,32 @@ impl<'a> ProductionLifecycleAdapter<'a> {
Ok(())
}
fn start_connectivity_helper(&mut self) -> Result<(), ProductionLifecycleError> {
fn start_connectivity_helper(&mut self) -> Result<(), LifecycleServiceError> {
let store = Arc::clone(&self.plan.store);
let network = NetworkCoordinator::new(&store);
self.awaiting_runtime
.as_mut()
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?
.ok_or_else(|| LifecycleServiceError("Runtime Supervisor is absent".into()))?
.receive_helper_staged(&network)
.map(|_| ())
.map_err(|error| failure(format!("starting connectivity helper: {error}")))
}
fn start_sidecar(&mut self) -> Result<(), ProductionLifecycleError> {
fn start_sidecar(&mut self) -> Result<(), LifecycleServiceError> {
self.awaiting_runtime
.as_mut()
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?
.ok_or_else(|| LifecycleServiceError("Runtime Supervisor is absent".into()))?
.start_sidecar()
.map_err(|error| failure(format!("starting sidecar: {error}")))
}
fn mark_network_ready(&mut self) -> Result<(), ProductionLifecycleError> {
fn mark_network_ready(&mut self) -> Result<(), LifecycleServiceError> {
let store = Arc::clone(&self.plan.store);
let network = NetworkCoordinator::new(&store);
let mut probe = self.plan.network_probe.clone();
self.awaiting_runtime
.as_mut()
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?
.ok_or_else(|| LifecycleServiceError("Runtime Supervisor is absent".into()))?
.receive_network_readiness(
&network,
self.plan.network_policy,
@ -370,13 +366,13 @@ impl<'a> ProductionLifecycleAdapter<'a> {
.map_err(|error| failure(format!("marking network ready: {error}")))
}
fn start_sandbox(&mut self) -> Result<(), ProductionLifecycleError> {
fn start_sandbox(&mut self) -> Result<(), LifecycleServiceError> {
let store = Arc::clone(&self.plan.store);
let sandbox = SandboxCoordinator::new(&store);
let mut pending = self
.awaiting_runtime
.take()
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?;
.ok_or_else(|| LifecycleServiceError("Runtime Supervisor is absent".into()))?;
let staged = pending
.receive_staged(&sandbox)
.map_err(|error| failure(format!("receiving attached runsc staging: {error}")))?;
@ -394,11 +390,11 @@ impl<'a> ProductionLifecycleAdapter<'a> {
Ok(())
}
fn stage_publication(&mut self) -> Result<(), ProductionLifecycleError> {
fn stage_publication(&mut self) -> Result<(), LifecycleServiceError> {
let mut anchor = self
.trust_anchor
.take()
.ok_or_else(|| ProductionLifecycleError("Trust Anchor is absent".into()))?;
.ok_or_else(|| LifecycleServiceError("Trust Anchor is absent".into()))?;
if self
.plan
.configuration
@ -421,7 +417,7 @@ impl<'a> ProductionLifecycleAdapter<'a> {
lazaret_core::identity::RequestId::new([0; 32]),
),
};
let (escalation, handle) = ProductionEscalation::new(ProductionEscalationInputs {
let (escalation, handle) = EscalationService::new(EscalationServiceInputs {
configuration: self.plan.configuration,
publication,
credentials,
@ -476,11 +472,11 @@ impl<'a> ProductionLifecycleAdapter<'a> {
Ok(())
}
fn expose_publication(&mut self) -> Result<(), ProductionLifecycleError> {
fn expose_publication(&mut self) -> Result<(), LifecycleServiceError> {
let published = self
.staged_control
.take()
.ok_or_else(|| ProductionLifecycleError("staged control face is absent".into()))?
.ok_or_else(|| LifecycleServiceError("staged control face is absent".into()))?
.expose()
.map_err(failure)?;
let ownership = published.recovery_ownership().map_err(failure)?;
@ -500,7 +496,7 @@ impl<'a> ProductionLifecycleAdapter<'a> {
Ok(())
}
fn clean_publication_after_withdrawal(&self) -> Result<(), ProductionLifecycleError> {
fn clean_publication_after_withdrawal(&self) -> Result<(), LifecycleServiceError> {
let generation_id = self.plan.generation_identity.generation_id;
let current = self
.plan
@ -513,7 +509,7 @@ impl<'a> ProductionLifecycleAdapter<'a> {
participant.generation_id() == generation_id
&& participant.role() == ParticipantRole::Publication
})
.ok_or_else(|| ProductionLifecycleError("Publication participant is absent".into()))?;
.ok_or_else(|| LifecycleServiceError("Publication participant is absent".into()))?;
let release = Participant::snapshot(
generation_id,
ParticipantRole::Publication,
@ -537,19 +533,19 @@ impl<'a> ProductionLifecycleAdapter<'a> {
.map_err(failure)
}
fn stop_sandbox(&mut self) -> Result<(), ProductionLifecycleError> {
fn stop_sandbox(&mut self) -> Result<(), LifecycleServiceError> {
let store = Arc::clone(&self.plan.store);
let sandbox = SandboxCoordinator::new(&store);
self.runtime
.as_mut()
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
.ok_or_else(|| LifecycleServiceError("committed runtime is absent".into()))?
.stop_sandbox(&sandbox)
.map_err(failure)?;
if self.plan.git_routing.is_some() {
let git_routing = GitRoutingCoordinator::new(&store);
self.runtime
.as_mut()
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
.ok_or_else(|| LifecycleServiceError("committed runtime is absent".into()))?
.stop_git_routing(&git_routing)
.map_err(failure)?;
self.git_routing_mount = None;
@ -557,7 +553,7 @@ impl<'a> ProductionLifecycleAdapter<'a> {
self.plan.runtime_staging.cleanup_overlay().map_err(failure)
}
fn stop_sidecar(&mut self) -> Result<(), ProductionLifecycleError> {
fn stop_sidecar(&mut self) -> Result<(), LifecycleServiceError> {
let store = Arc::clone(&self.plan.store);
let network = NetworkCoordinator::new(&store);
network
@ -565,7 +561,7 @@ impl<'a> ProductionLifecycleAdapter<'a> {
.map_err(failure)?;
self.runtime
.as_mut()
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
.ok_or_else(|| LifecycleServiceError("committed runtime is absent".into()))?
.stop_sidecar()
.map_err(failure)?;
network
@ -573,7 +569,7 @@ impl<'a> ProductionLifecycleAdapter<'a> {
.map_err(failure)
}
fn unmount_state_anchor(&mut self) -> Result<(), ProductionLifecycleError> {
fn unmount_state_anchor(&mut self) -> Result<(), LifecycleServiceError> {
let coordinator = self.state_anchor();
self.state_anchor_release = Some(
coordinator
@ -586,11 +582,11 @@ impl<'a> ProductionLifecycleAdapter<'a> {
let runtime = self
.runtime
.as_mut()
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?;
.ok_or_else(|| LifecycleServiceError("committed runtime is absent".into()))?;
let release = self
.state_anchor_release
.as_ref()
.ok_or_else(|| ProductionLifecycleError("State Anchor release is absent".into()))?;
.ok_or_else(|| LifecycleServiceError("State Anchor release is absent".into()))?;
runtime
.unmount_state_anchor(release.mount())
.map_err(failure)?;
@ -607,18 +603,18 @@ impl<'a> ProductionLifecycleAdapter<'a> {
Ok(())
}
fn release_prepared_lower(&mut self) -> Result<(), ProductionLifecycleError> {
fn release_prepared_lower(&mut self) -> Result<(), LifecycleServiceError> {
let release = self
.state_anchor_release
.as_ref()
.ok_or_else(|| ProductionLifecycleError("State Anchor release is absent".into()))?;
.ok_or_else(|| LifecycleServiceError("State Anchor release is absent".into()))?;
let first_observation = self.state_anchor_absence.ok_or_else(|| {
ProductionLifecycleError("State Anchor absence evidence is absent".into())
LifecycleServiceError("State Anchor absence evidence is absent".into())
})?;
let runtime = self
.runtime
.as_mut()
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?;
.ok_or_else(|| LifecycleServiceError("committed runtime is absent".into()))?;
let coordinator = StateAnchorCoordinator::new(&self.plan.store, self.plan.catalog);
coordinator
.complete_release_after_unmount(
@ -650,17 +646,17 @@ impl<'a> ProductionLifecycleAdapter<'a> {
Ok(())
}
fn graceful_exit(&mut self) -> Result<(), ProductionLifecycleError> {
fn graceful_exit(&mut self) -> Result<(), LifecycleServiceError> {
self.runtime
.take()
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
.ok_or_else(|| LifecycleServiceError("committed runtime is absent".into()))?
.graceful_exit()
.map_err(failure)
}
}
impl LifecycleAdapter for ProductionLifecycleAdapter<'_> {
type Error = ProductionLifecycleError;
impl LifecycleAdapter for CoordinatorLifecycleAdapter<'_> {
type Error = LifecycleServiceError;
fn execute(
&mut self,
@ -721,13 +717,11 @@ impl LifecycleAdapter for ProductionLifecycleAdapter<'_> {
}
}
/// Serves one production control connection through the exact retained lifecycle owner.
/// Serves one control connection through the exact retained lifecycle owner.
///
/// # Errors
/// Refuses absent publication, poisoned lifecycle ownership, or control/session failure.
pub fn serve_production_control(
lifecycle: &Mutex<ProductionHostCoordinatorLifecycle<'_, '_>>,
) -> Result<(), ControlError> {
pub fn serve_control(lifecycle: &Mutex<CoordinatorLifecycle<'_, '_>>) -> Result<(), ControlError> {
let face = lifecycle
.lock()
.map_err(|_| ControlError::LockPoisoned)?
@ -735,22 +729,22 @@ pub fn serve_production_control(
.published_control()
.ok_or(ControlError::StaleGeneration)?;
let connection = face.accept_connection()?;
serve_production_control_connection(lifecycle, &face, connection)
serve_control_connection(lifecycle, &face, connection)
}
pub(crate) fn serve_production_control_connection(
lifecycle: &Mutex<ProductionHostCoordinatorLifecycle<'_, '_>>,
pub(crate) fn serve_control_connection(
lifecycle: &Mutex<CoordinatorLifecycle<'_, '_>>,
face: &PublishedControlFace,
connection: AcceptedControlConnection,
) -> Result<(), ControlError> {
serve_production_control_connection_with_escalation(lifecycle, face, connection, None)
serve_control_connection_with_escalation(lifecycle, face, connection, None)
}
pub(crate) fn serve_production_control_connection_with_escalation(
lifecycle: &Mutex<ProductionHostCoordinatorLifecycle<'_, '_>>,
pub(crate) fn serve_control_connection_with_escalation(
lifecycle: &Mutex<CoordinatorLifecycle<'_, '_>>,
face: &PublishedControlFace,
connection: AcceptedControlConnection,
escalation: Option<&ProductionEscalationHandle>,
escalation: Option<&EscalationServiceHandle>,
) -> Result<(), ControlError> {
face.serve_connection_coordinated(
connection,

View file

@ -17,17 +17,15 @@ use lazaret_core::identity::{
};
use lazaret_core::records::{Domain, Session, SessionPhase};
use lazaret_linux::filesystem::try_exclusive_flock;
use lazaret_runtime_supervisor::sandbox::production_sandbox_layout;
use lazaret_runtime_supervisor::sandbox::default_sandbox_layout;
use rustix::fs::{Mode, OFlags, openat};
use crate::bootstrap::{
ProductionHostCoordinatorLifecycle, serve_production_control_connection_with_escalation,
};
use crate::bootstrap::{CoordinatorLifecycle, serve_control_connection_with_escalation};
use crate::cold_start::{BootstrapOwnerGuard, ColdStart, ColdStartError};
use crate::control::ControlError;
use crate::domain_store::HostCoordinatorDomainStore;
use crate::escalation::{ProductionEscalation, ProductionEscalationHandle};
use crate::production::{BootstrapOwnerGuard, ProductionColdStart, ProductionColdStartError};
use crate::production_selector::{InstalledProductPaths, select_production_cold_start};
use crate::escalation::{EscalationService, EscalationServiceHandle};
use crate::runtime_selector::{InstalledProductPaths, select_runtime};
use crate::waiter::BootstrapWaiterFace;
pub const PRIVATE_CLIENT_BOOTSTRAP_ROLE: &str = "--lazaret-private-client-bootstrap-v1";
@ -135,7 +133,7 @@ fn run_private_client_bootstrap(arguments: &[OsString]) -> Result<(), PrivateCli
.build()
.map_err(failure)?;
let selection = runtime
.block_on(select_production_cold_start(
.block_on(select_runtime(
&configuration,
Arc::clone(&store),
InstalledProductPaths {
@ -154,18 +152,18 @@ fn run_private_client_bootstrap(arguments: &[OsString]) -> Result<(), PrivateCli
session_anchor_id: parsed.session_anchor_id,
session_phase: SessionPhase::New,
};
ProductionColdStart::stage(inputs)
ColdStart::stage(inputs)
.map_err(failure)?
.run(bootstrap_owner, &owner, serve_published)
.map_err(failure)
}
fn serve_published(
lifecycle: &Mutex<ProductionHostCoordinatorLifecycle<'_, '_>>,
lifecycle: &Mutex<CoordinatorLifecycle<'_, '_>>,
waiters: &RefCell<BootstrapWaiterFace>,
bootstrap_owner: &BootstrapOwnerGuard,
bootstrap_owner_session_id: SessionId,
) -> Result<(), ProductionColdStartError> {
) -> Result<(), ColdStartError> {
let escalation = take_escalation(lifecycle)?;
std::thread::scope(|scope| {
let escalation_enabled = escalation.is_some();
@ -187,9 +185,9 @@ fn serve_published(
}
require_escalation_running(&escalation_completions, escalation_enabled)?;
let face = {
let mut lifecycle = lifecycle.lock().map_err(|_| {
ProductionColdStartError::service("production lifecycle lock is poisoned")
})?;
let mut lifecycle = lifecycle
.lock()
.map_err(|_| ColdStartError::service("lifecycle lock is poisoned"))?;
let Some(face) = lifecycle.adapter().published_control() else {
for socket in active.values() {
let _ = socket.shutdown(std::net::Shutdown::Both);
@ -202,23 +200,21 @@ fn serve_published(
escalation.stop();
escalation_completions
.recv()
.map_err(ProductionColdStartError::service)?
.map_err(ProductionColdStartError::service)?;
.map_err(ColdStartError::service)?
.map_err(ColdStartError::service)?;
}
return Ok(());
};
lifecycle
.reconcile_published_bootstrap_owner(
bootstrap_owner_session_id,
bootstrap_owner
.is_live()
.map_err(ProductionColdStartError::service)?,
bootstrap_owner.is_live().map_err(ColdStartError::service)?,
)
.map_err(ProductionColdStartError::service)?;
.map_err(ColdStartError::service)?;
waiters
.borrow_mut()
.reconcile_published(&mut lifecycle)
.map_err(ProductionColdStartError::service)?;
.map_err(ColdStartError::service)?;
face
};
if active.len() == MAX_CONCURRENT_CONNECTIONS {
@ -229,16 +225,16 @@ fn serve_published(
Ok(connection) => {
let cancellation = connection
.cancellation_handle()
.map_err(ProductionColdStartError::service)?;
.map_err(ColdStartError::service)?;
let connection_id = next_connection;
next_connection = next_connection.checked_add(1).ok_or_else(|| {
ProductionColdStartError::service("control connection counter overflow")
ColdStartError::service("control connection counter overflow")
})?;
active.insert(connection_id, cancellation);
let completed = completed.clone();
let escalation = escalation_handle.clone();
scope.spawn(move || {
let result = serve_production_control_connection_with_escalation(
let result = serve_control_connection_with_escalation(
lifecycle,
&face,
connection,
@ -252,44 +248,41 @@ fn serve_published(
{
std::thread::sleep(std::time::Duration::from_millis(10));
}
Err(error) => return Err(ProductionColdStartError::service(error)),
Err(error) => return Err(ColdStartError::service(error)),
}
}
})
}
fn take_escalation<'resources>(
lifecycle: &Mutex<ProductionHostCoordinatorLifecycle<'_, 'resources>>,
) -> Result<
Option<(ProductionEscalation<'resources>, ProductionEscalationHandle)>,
ProductionColdStartError,
> {
lifecycle: &Mutex<CoordinatorLifecycle<'_, 'resources>>,
) -> Result<Option<(EscalationService<'resources>, EscalationServiceHandle)>, ColdStartError> {
let mut lifecycle = lifecycle
.lock()
.map_err(|_| ProductionColdStartError::service("production lifecycle lock is poisoned"))?;
.map_err(|_| ColdStartError::service("lifecycle lock is poisoned"))?;
lifecycle
.adapter()
.published_control()
.ok_or_else(|| ProductionColdStartError::service("control face is absent"))?
.ok_or_else(|| ColdStartError::service("control face is absent"))?
.set_nonblocking(true)
.map_err(ProductionColdStartError::service)?;
.map_err(ColdStartError::service)?;
Ok(lifecycle.adapter_mut().take_escalation())
}
fn require_escalation_running(
completions: &std::sync::mpsc::Receiver<Result<(), crate::escalation::EscalationError>>,
enabled: bool,
) -> Result<(), ProductionColdStartError> {
) -> Result<(), ColdStartError> {
match completions.try_recv() {
Ok(result) => {
result.map_err(ProductionColdStartError::service)?;
Err(ProductionColdStartError::service(
"production escalation service stopped before teardown",
result.map_err(ColdStartError::service)?;
Err(ColdStartError::service(
"escalation service stopped before teardown",
))
}
Err(std::sync::mpsc::TryRecvError::Disconnected) if enabled => Err(
ProductionColdStartError::service("production escalation service was abandoned"),
),
Err(std::sync::mpsc::TryRecvError::Disconnected) if enabled => {
Err(ColdStartError::service("escalation service was abandoned"))
}
Err(std::sync::mpsc::TryRecvError::Empty | std::sync::mpsc::TryRecvError::Disconnected) => {
Ok(())
}
@ -299,15 +292,13 @@ fn require_escalation_running(
fn wait_for_connection_completion(
completions: &std::sync::mpsc::Receiver<(u64, Result<(), ControlError>)>,
active: &mut BTreeMap<u64, std::os::unix::net::UnixStream>,
) -> Result<(), ProductionColdStartError> {
let (connection_id, result) = completions
.recv()
.map_err(ProductionColdStartError::service)?;
) -> Result<(), ColdStartError> {
let (connection_id, result) = completions.recv().map_err(ColdStartError::service)?;
active.remove(&connection_id);
connection_completion(result)
}
fn connection_completion(result: Result<(), ControlError>) -> Result<(), ProductionColdStartError> {
fn connection_completion(result: Result<(), ControlError>) -> Result<(), ColdStartError> {
match result {
Ok(()) => Ok(()),
Err(ControlError::Io(error))
@ -322,7 +313,7 @@ fn connection_completion(result: Result<(), ControlError>) -> Result<(), Product
{
Ok(())
}
Err(error) => Err(ProductionColdStartError::service(error)),
Err(error) => Err(ColdStartError::service(error)),
}
}
@ -470,7 +461,7 @@ fn load_configuration(
xdg_data_home: optional_environment_path("XDG_DATA_HOME"),
xdg_runtime_dir: runtime,
environment: environment(),
sandbox_layout: production_sandbox_layout()?,
sandbox_layout: default_sandbox_layout()?,
})?)
}

File diff suppressed because it is too large Load diff

View file

@ -993,7 +993,7 @@ struct SessionAuthorityFiles {
registration_identity: MailboxEntryIdentity,
}
struct ProductionSessionAuthority {
struct SessionEscalationAuthority {
session: VerifiedSession,
files: SessionAuthorityFiles,
}
@ -1004,7 +1004,7 @@ struct RegistrationCommand {
completed: mpsc::SyncSender<Result<(), String>>,
}
enum ProductionEscalationCommand {
enum EscalationServiceCommand {
Register(Box<RegistrationCommand>),
Disconnect {
session_id: SessionId,
@ -1014,11 +1014,11 @@ enum ProductionEscalationCommand {
}
#[derive(Clone)]
pub(crate) struct ProductionEscalationHandle {
commands: mpsc::Sender<ProductionEscalationCommand>,
pub(crate) struct EscalationServiceHandle {
commands: mpsc::Sender<EscalationServiceCommand>,
}
impl ProductionEscalationHandle {
impl EscalationServiceHandle {
pub(crate) fn register(
&self,
binding: SessionBinding,
@ -1026,40 +1026,40 @@ impl ProductionEscalationHandle {
) -> Result<(), EscalationError> {
let (completed, result) = mpsc::sync_channel(1);
self.commands
.send(ProductionEscalationCommand::Register(Box::new(
.send(EscalationServiceCommand::Register(Box::new(
RegistrationCommand {
binding,
registration,
completed,
},
)))
.map_err(|_| io::Error::other("production escalation service stopped"))?;
.map_err(|_| io::Error::other("escalation service stopped"))?;
result
.recv()
.map_err(|_| io::Error::other("production escalation registration was abandoned"))?
.map_err(|_| io::Error::other("escalation registration was abandoned"))?
.map_err(|error| io::Error::other(error).into())
}
pub(crate) fn disconnect(&self, session_id: SessionId) -> Result<(), EscalationError> {
let (completed, result) = mpsc::sync_channel(1);
self.commands
.send(ProductionEscalationCommand::Disconnect {
.send(EscalationServiceCommand::Disconnect {
session_id,
completed,
})
.map_err(|_| io::Error::other("production escalation service stopped"))?;
.map_err(|_| io::Error::other("escalation service stopped"))?;
result
.recv()
.map_err(|_| io::Error::other("production escalation disconnect was abandoned"))?
.map_err(|_| io::Error::other("escalation disconnect was abandoned"))?
.map_err(|error| io::Error::other(error).into())
}
pub(crate) fn stop(&self) {
let _ = self.commands.send(ProductionEscalationCommand::Stop);
let _ = self.commands.send(EscalationServiceCommand::Stop);
}
}
pub(crate) struct ProductionEscalation<'a> {
pub(crate) struct EscalationService<'a> {
configuration: &'a ResolvedDomainConfiguration,
publication: ProtocolPublicationIdentity,
credentials: SandboxCredentialBundle,
@ -1068,12 +1068,12 @@ pub(crate) struct ProductionEscalation<'a> {
prompt_transport: Arc<SessionPromptTransport>,
coordinator: EscalationCoordinator,
ownership: LocalExecutionOwnership,
sessions: BTreeMap<SessionId, ProductionSessionAuthority>,
sessions: BTreeMap<SessionId, SessionEscalationAuthority>,
active: BTreeMap<RequestId, SessionId>,
commands: mpsc::Receiver<ProductionEscalationCommand>,
commands: mpsc::Receiver<EscalationServiceCommand>,
}
pub(crate) struct ProductionEscalationInputs<'a> {
pub(crate) struct EscalationServiceInputs<'a> {
pub configuration: &'a ResolvedDomainConfiguration,
pub publication: ProtocolPublicationIdentity,
pub credentials: SandboxCredentialBundle,
@ -1086,10 +1086,10 @@ pub(crate) struct ProductionEscalationInputs<'a> {
pub invoking_uid: u32,
}
impl<'a> ProductionEscalation<'a> {
impl<'a> EscalationService<'a> {
pub(crate) fn new(
inputs: ProductionEscalationInputs<'a>,
) -> Result<(Self, ProductionEscalationHandle), EscalationError> {
inputs: EscalationServiceInputs<'a>,
) -> Result<(Self, EscalationServiceHandle), EscalationError> {
inputs.credential_files.publish(
OsStr::new("publication.postcard"),
&lazaret_core::codec::encode(&inputs.publication)
@ -1122,7 +1122,7 @@ impl<'a> ProductionEscalation<'a> {
active: BTreeMap::new(),
commands: receiver,
},
ProductionEscalationHandle { commands },
EscalationServiceHandle { commands },
))
}
@ -1141,10 +1141,10 @@ impl<'a> ProductionEscalation<'a> {
fn apply_command(
&mut self,
command: ProductionEscalationCommand,
command: EscalationServiceCommand,
) -> Result<bool, EscalationError> {
match command {
ProductionEscalationCommand::Register(command) => {
EscalationServiceCommand::Register(command) => {
let RegistrationCommand {
binding,
registration,
@ -1160,7 +1160,7 @@ impl<'a> ProductionEscalation<'a> {
}
Ok(false)
}
ProductionEscalationCommand::Disconnect {
EscalationServiceCommand::Disconnect {
session_id,
completed,
} => {
@ -1174,7 +1174,7 @@ impl<'a> ProductionEscalation<'a> {
}
Ok(false)
}
ProductionEscalationCommand::Stop => {
EscalationServiceCommand::Stop => {
let sessions = self.sessions.keys().copied().collect::<Vec<_>>();
for session_id in sessions {
self.disconnect(session_id)?;
@ -1226,7 +1226,7 @@ impl<'a> ProductionEscalation<'a> {
};
self.sessions.insert(
session_id,
ProductionSessionAuthority {
SessionEscalationAuthority {
session,
files: SessionAuthorityFiles {
binding_name,

View file

@ -2,16 +2,16 @@
pub mod bootstrap;
pub mod client_bootstrap;
pub mod cold_start;
pub mod control;
pub mod domain_store;
pub mod escalation;
pub mod git_routing;
pub mod lifecycle;
pub mod network;
pub mod production;
pub mod production_selector;
pub mod recovery;
pub mod runtime;
pub mod runtime_selector;
pub mod sandbox_init;
pub mod state_anchor;
pub mod state_maintenance;

View file

@ -276,7 +276,7 @@ where
/// Drives one cold-start leader through the complete normal publication boundary.
///
/// # Errors
/// Returns the first invalid transition, durable update, or production adapter failure.
/// Returns the first invalid transition, durable update, or lifecycle adapter failure.
pub fn bootstrap_new_domain(
&mut self,
generation: Generation,
@ -291,7 +291,7 @@ where
/// Returns `false` after applying the modeled bootstrap-owner disconnect transition.
///
/// # Errors
/// Returns the first invalid transition, durable update, or production adapter failure.
/// Returns the first invalid transition, durable update, or lifecycle adapter failure.
pub fn bootstrap_new_domain_while_owner_live(
&mut self,
generation: Generation,

View file

@ -565,7 +565,7 @@ fn require_mount_absent(
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum ProductionRecoveryBoundary {
pub(crate) enum RecoveryBoundary {
Snapshot,
Publication,
Sandbox,
@ -578,22 +578,16 @@ pub(crate) enum ProductionRecoveryBoundary {
FinishGeneration,
}
pub(crate) fn recover_production_before_cold_start(
pub(crate) fn recover_before_cold_start(
configuration: &ResolvedDomainConfiguration,
store: &HostCoordinatorDomainStore,
catalog: &Catalog,
prepared: &PreparedLowerStore,
) -> Result<(), String> {
recover_production_before_cold_start_with(
configuration,
store,
catalog,
prepared,
|_, _| Ok(()),
)
recover_before_cold_start_with(configuration, store, catalog, prepared, |_, _| Ok(()))
}
fn recover_production_before_cold_start_with<Failpoint>(
fn recover_before_cold_start_with<Failpoint>(
configuration: &ResolvedDomainConfiguration,
store: &HostCoordinatorDomainStore,
catalog: &Catalog,
@ -601,9 +595,9 @@ fn recover_production_before_cold_start_with<Failpoint>(
failpoint: Failpoint,
) -> Result<(), String>
where
Failpoint: FnMut(ProductionRecoveryBoundary, GenerationId) -> Result<(), String>,
Failpoint: FnMut(RecoveryBoundary, GenerationId) -> Result<(), String>,
{
recover_production_before_cold_start_with_opener(
recover_before_cold_start_with_opener(
configuration,
store,
catalog,
@ -614,7 +608,7 @@ where
)
}
fn recover_production_before_cold_start_with_opener<Failpoint, Open, Delete>(
fn recover_before_cold_start_with_opener<Failpoint, Open, Delete>(
configuration: &ResolvedDomainConfiguration,
store: &HostCoordinatorDomainStore,
catalog: &Catalog,
@ -624,12 +618,12 @@ fn recover_production_before_cold_start_with_opener<Failpoint, Open, Delete>(
mut delete: Delete,
) -> Result<(), String>
where
Failpoint: FnMut(ProductionRecoveryBoundary, GenerationId) -> Result<(), String>,
Failpoint: FnMut(RecoveryBoundary, GenerationId) -> Result<(), String>,
Open: FnMut(PidfsProcessIdentityV1) -> io::Result<PidfsRecoveryMatch>,
Delete: FnMut(&std::os::fd::OwnedFd, &str) -> Result<(), String>,
{
recover_state_reset(configuration, store).map_err(|error| error.to_string())?;
failpoint(ProductionRecoveryBoundary::Snapshot, GenerationId::new(0))?;
failpoint(RecoveryBoundary::Snapshot, GenerationId::new(0))?;
let snapshot = store
.snapshot()
.map_err(|error| format!("Domain Store snapshot failed: {error}"))?;
@ -661,14 +655,11 @@ where
)
})?;
}
failpoint(
ProductionRecoveryBoundary::PrepareOperations,
GenerationId::new(0),
)?;
failpoint(RecoveryBoundary::PrepareOperations, GenerationId::new(0))?;
prepared
.recover_preparations()
.map_err(|error| error.to_string())?;
failpoint(ProductionRecoveryBoundary::ArtifactGc, GenerationId::new(0))?;
failpoint(RecoveryBoundary::ArtifactGc, GenerationId::new(0))?;
let catalog_snapshot = catalog.snapshot().map_err(|error| error.to_string())?;
let selected = catalog_snapshot
.prepared_artifacts
@ -697,7 +688,7 @@ fn recover_generation<Failpoint, Open, Delete>(
delete: &mut Delete,
) -> Result<(), String>
where
Failpoint: FnMut(ProductionRecoveryBoundary, GenerationId) -> Result<(), String>,
Failpoint: FnMut(RecoveryBoundary, GenerationId) -> Result<(), String>,
Open: FnMut(PidfsProcessIdentityV1) -> io::Result<PidfsRecoveryMatch>,
Delete: FnMut(&std::os::fd::OwnedFd, &str) -> Result<(), String>,
{
@ -744,11 +735,11 @@ where
let mut owners = seal_runtime_owners(&ownership, open)
.map_err(|error| format!("runtime owner acquisition failed: {error}"))?;
failpoint(ProductionRecoveryBoundary::Publication, generation_id)?;
failpoint(RecoveryBoundary::Publication, generation_id)?;
recover_publication(configuration, store, generation_id)
.map_err(|error| format!("Publication recovery failed: {error}"))?;
failpoint(ProductionRecoveryBoundary::Sandbox, generation_id)?;
failpoint(RecoveryBoundary::Sandbox, generation_id)?;
reconcile_runtime_before_state_anchor(&mut owners)
.map_err(|error| format!("runtime-owner reconciliation failed: {error}"))?;
require_sandbox_absent(configuration, store, generation_id, delete)
@ -756,13 +747,13 @@ where
clean_role_after_absence(store, generation_id, ParticipantRole::Sandbox)
.map_err(|error| format!("Sandbox cleanup failed: {error}"))?;
failpoint(ProductionRecoveryBoundary::HelperAndNetwork, generation_id)?;
failpoint(RecoveryBoundary::HelperAndNetwork, generation_id)?;
clean_role_after_absence(store, generation_id, ParticipantRole::Helper)
.map_err(|error| format!("Helper cleanup failed: {error}"))?;
clean_role_after_absence(store, generation_id, ParticipantRole::Network)
.map_err(|error| format!("Network cleanup failed: {error}"))?;
failpoint(ProductionRecoveryBoundary::GitRouting, generation_id)?;
failpoint(RecoveryBoundary::GitRouting, generation_id)?;
require_mount_absent(
ownership.git_routing_path.as_ref(),
ownership.git_routing_mount,
@ -772,11 +763,11 @@ where
clean_role_after_absence(store, generation_id, ParticipantRole::GitRouting)
.map_err(|error| format!("Git-routing cleanup failed: {error}"))?;
failpoint(ProductionRecoveryBoundary::StateAnchor, generation_id)?;
failpoint(RecoveryBoundary::StateAnchor, generation_id)?;
recover_state_anchor(configuration, store, catalog, generation, &mut owners)
.map_err(|error| format!("State Anchor recovery failed: {error}"))?;
failpoint(ProductionRecoveryBoundary::PreparedLower, generation_id)?;
failpoint(RecoveryBoundary::PreparedLower, generation_id)?;
recover_partial_prepared_lower(configuration, store, catalog, generation_id)
.map_err(|error| format!("prepared-lower recovery failed: {error}"))?;
@ -787,7 +778,7 @@ where
}) {
return Err("generation retains non-Clean participants after recovery".into());
}
failpoint(ProductionRecoveryBoundary::FinishGeneration, generation_id)?;
failpoint(RecoveryBoundary::FinishGeneration, generation_id)?;
let mut clean = generation.clone();
clean.durable_decision = DurableDecision::Clean;
store
@ -1341,7 +1332,7 @@ mod tests {
SandboxOwnership, StateAnchorOwnership,
};
use lazaret_linux::filesystem::try_exclusive_flock;
use lazaret_runtime_supervisor::sandbox::production_sandbox_layout;
use lazaret_runtime_supervisor::sandbox::default_sandbox_layout;
use super::*;
@ -1504,8 +1495,8 @@ mod tests {
}
#[test]
fn production_preflight_recovers_full_runtime_then_is_idempotent() {
let fixture = ProductionRecoveryFixture::new();
fn recovery_preflight_recovers_full_runtime_then_is_idempotent() {
let fixture = RecoveryFixture::new();
let mut boundaries = Vec::new();
fixture
.recover_with(|boundary, generation| {
@ -1516,7 +1507,7 @@ mod tests {
assert!(
boundaries
.iter()
.any(|(boundary, _)| { *boundary == ProductionRecoveryBoundary::StateAnchor })
.any(|(boundary, _)| { *boundary == RecoveryBoundary::StateAnchor })
);
assert_eq!(
fixture.store.snapshot().unwrap().generations[0].durable_decision,
@ -1552,7 +1543,7 @@ mod tests {
.exists()
);
recover_production_before_cold_start(
recover_before_cold_start(
&fixture.configuration,
&fixture.store,
&fixture.catalog,
@ -1570,12 +1561,12 @@ mod tests {
#[test]
fn bare_publication_intent_requires_every_fixed_label_to_be_absent() {
let absent = ProductionRecoveryFixture::new();
let absent = RecoveryFixture::new();
absent.prepare_bare_publication(PublicationFixtureBoundary::TrulyAbsent);
recover_publication(&absent.configuration, &absent.store, GenerationId::new(2)).unwrap();
assert_eq!(absent.publication(2).state(), ParticipantState::Clean);
let identity = ProductionRecoveryFixture::new();
let identity = RecoveryFixture::new();
identity.prepare_bare_publication(PublicationFixtureBoundary::AfterStagedIdentity);
assert!(
recover_publication(
@ -1594,7 +1585,7 @@ mod tests {
assert_eq!(identity.publication(2).state(), ParticipantState::Intent);
assert!(identity.publication(2).ownership().is_none());
let socket = ProductionRecoveryFixture::new();
let socket = RecoveryFixture::new();
socket.prepare_bare_publication(PublicationFixtureBoundary::AfterStagedSocket);
assert!(
recover_publication(&socket.configuration, &socket.store, GenerationId::new(2))
@ -1617,27 +1608,27 @@ mod tests {
}
#[test]
fn production_preflight_retries_every_recovery_boundary() {
fn recovery_preflight_retries_every_recovery_boundary() {
for failed in [
ProductionRecoveryBoundary::Snapshot,
ProductionRecoveryBoundary::Publication,
ProductionRecoveryBoundary::Sandbox,
ProductionRecoveryBoundary::HelperAndNetwork,
ProductionRecoveryBoundary::GitRouting,
ProductionRecoveryBoundary::StateAnchor,
ProductionRecoveryBoundary::PreparedLower,
ProductionRecoveryBoundary::PrepareOperations,
ProductionRecoveryBoundary::ArtifactGc,
ProductionRecoveryBoundary::FinishGeneration,
RecoveryBoundary::Snapshot,
RecoveryBoundary::Publication,
RecoveryBoundary::Sandbox,
RecoveryBoundary::HelperAndNetwork,
RecoveryBoundary::GitRouting,
RecoveryBoundary::StateAnchor,
RecoveryBoundary::PreparedLower,
RecoveryBoundary::PrepareOperations,
RecoveryBoundary::ArtifactGc,
RecoveryBoundary::FinishGeneration,
] {
let fixture = ProductionRecoveryFixture::new();
let fixture = RecoveryFixture::new();
let mut injected = false;
assert!(
fixture
.recover_with(|boundary, _| {
if boundary == failed && !injected {
injected = true;
Err("injected production recovery boundary".into())
Err("injected recovery boundary".into())
} else {
Ok(())
}
@ -1654,10 +1645,10 @@ mod tests {
}
#[test]
fn production_preflight_opens_every_exact_owner_once() {
let fixture = ProductionRecoveryFixture::new();
fn recovery_preflight_opens_every_exact_owner_once() {
let fixture = RecoveryFixture::new();
let mut opened = BTreeMap::new();
recover_production_before_cold_start_with_opener(
recover_before_cold_start_with_opener(
&fixture.configuration,
&fixture.store,
&fixture.catalog,
@ -1678,7 +1669,7 @@ mod tests {
#[test]
fn publication_replacement_and_runsc_root_replacement_refuse_untouched() {
let fixture = ProductionRecoveryFixture::new();
let fixture = RecoveryFixture::new();
let runtime_domain = Path::new(std::ffi::OsStr::from_bytes(
fixture
.configuration
@ -1701,7 +1692,7 @@ mod tests {
assert!(fixture.recover_with(|_, _| Ok(())).is_err());
assert!(socket.exists());
let fixture = ProductionRecoveryFixture::new();
let fixture = RecoveryFixture::new();
let runtime_domain = Path::new(std::ffi::OsStr::from_bytes(
fixture
.configuration
@ -1720,7 +1711,7 @@ mod tests {
.is_symlink()
);
let fixture = ProductionRecoveryFixture::new();
let fixture = RecoveryFixture::new();
let runtime_domain = Path::new(std::ffi::OsStr::from_bytes(
fixture
.configuration
@ -1734,7 +1725,7 @@ mod tests {
assert!(fixture.recover_with(|_, _| Ok(())).is_err());
assert_eq!(fs::read(&identity).unwrap(), b"replacement");
let fixture = ProductionRecoveryFixture::new();
let fixture = RecoveryFixture::new();
let runtime_domain = Path::new(std::ffi::OsStr::from_bytes(
fixture
.configuration
@ -1747,7 +1738,7 @@ mod tests {
fs::rename(&runsc, &old_runsc).unwrap();
fs::create_dir(&runsc).unwrap();
let mut deletes = 0;
let result = recover_production_before_cold_start_with_opener(
let result = recover_before_cold_start_with_opener(
&fixture.configuration,
&fixture.store,
&fixture.catalog,
@ -1763,7 +1754,7 @@ mod tests {
assert_eq!(deletes, 0);
assert!(runsc.exists());
let fixture = ProductionRecoveryFixture::new();
let fixture = RecoveryFixture::new();
let runtime_domain = Path::new(std::ffi::OsStr::from_bytes(
fixture
.configuration
@ -1783,7 +1774,7 @@ mod tests {
);
}
struct ProductionRecoveryFixture {
struct RecoveryFixture {
_tree: tempfile::TempDir,
configuration: ResolvedDomainConfiguration,
store: HostCoordinatorDomainStore,
@ -1791,12 +1782,12 @@ mod tests {
prepared: PreparedLowerStore,
}
impl ProductionRecoveryFixture {
impl RecoveryFixture {
fn recover_with<Failpoint>(&self, failpoint: Failpoint) -> Result<(), String>
where
Failpoint: FnMut(ProductionRecoveryBoundary, GenerationId) -> Result<(), String>,
Failpoint: FnMut(RecoveryBoundary, GenerationId) -> Result<(), String>,
{
recover_production_before_cold_start_with_opener(
recover_before_cold_start_with_opener(
&self.configuration,
&self.store,
&self.catalog,
@ -2221,7 +2212,7 @@ mod tests {
xdg_data_home: None,
xdg_runtime_dir: runtime,
environment: BTreeMap::new(),
sandbox_layout: production_sandbox_layout().unwrap(),
sandbox_layout: default_sandbox_layout().unwrap(),
})
.unwrap()
}

View file

@ -0,0 +1,629 @@
use std::collections::BTreeSet;
use std::fmt;
use std::fs::File;
use std::os::unix::ffi::OsStringExt;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use lazaret_core::configuration::ResolvedDomainConfiguration;
use lazaret_core::identity::{
ConfigurationFingerprint, ContainerId, CoordinatorId, DomainId, GenerationId, NetworkSidecarId,
PrepareOperationId, SandboxId,
};
use lazaret_core::network::{DnsQueryExpectation, ReachabilityExpectation};
use lazaret_core::records::{DurableDecision, Generation, IpAddress, PreparedArtifact, VolumeKind};
use lazaret_linux::filesystem::namespace_identity;
use lazaret_linux::platform::{PlatformObservation, check_platform};
use lazaret_prepared_lower::{
IdMap, IdMapRange, OciRegistryTransport, Platform, PreparedDestination,
PreparedDestinationKind, PreparedLowerStore, RegistryCredentials,
};
use lazaret_runtime_supervisor::network::{DnsProbeTarget, NetworkProbeRequest, TcpProbeTarget};
use lazaret_runtime_supervisor::sandbox::{
CREDENTIALS_DESTINATION, ESCALATION_CLIENT_DESTINATION, MAILBOX_REQUESTS_DESTINATION,
MAILBOX_RESULTS_DESTINATION, RootProcess, SANDBOX_INIT_DESTINATION, TERMINFO_ROOT_DESTINATION,
WORKSPACE_DESTINATION,
};
use lazaret_storage_catalog::{Catalog, CatalogError};
use rustix::fs::{Mode, OFlags, ResolveFlags, openat2};
use crate::cold_start::ColdStartInputs;
use crate::domain_store::HostCoordinatorDomainStore;
use crate::recovery::recover_before_cold_start;
use crate::sandbox_init::PRIVATE_SANDBOX_INIT_ROLE;
const NETWORK_NAMESPACE_PATH: &str = "/proc/self/ns/net";
#[derive(Debug)]
pub struct RuntimeSelectionError(String);
impl fmt::Display for RuntimeSelectionError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
impl std::error::Error for RuntimeSelectionError {}
fn failure(error: impl fmt::Display) -> RuntimeSelectionError {
RuntimeSelectionError(error.to_string())
}
pub struct InstalledProductPaths<'a> {
pub escalation_client: &'a Path,
pub sandbox_init: &'a Path,
}
pub struct RuntimeSelection {
store: Arc<HostCoordinatorDomainStore>,
domain_id: DomainId,
configuration_fingerprint: ConfigurationFingerprint,
catalog: Catalog,
artifact: PreparedArtifact,
artifact_authority: File,
artifact_label: PathBuf,
generation: Generation,
resolver_addresses: Vec<IpAddress>,
network_probe: NetworkProbeRequest,
reachability: ReachabilityExpectation,
container_id: ContainerId,
escalation_client: File,
sandbox_init: File,
}
impl RuntimeSelection {
/// Borrows the selected resources as the existing cold-start input.
///
/// # Errors
/// Refuses a configuration other than the exact one used for selection.
pub fn cold_start_inputs<'a>(
&'a self,
configuration: &'a ResolvedDomainConfiguration,
) -> Result<ColdStartInputs<'a>, RuntimeSelectionError> {
if configuration.domain_id() != self.domain_id
|| configuration.configuration_fingerprint() != self.configuration_fingerprint
{
return Err(failure("selected cold start does not match configuration"));
}
Ok(ColdStartInputs {
store: Arc::clone(&self.store),
catalog: &self.catalog,
configuration,
generation: self.generation.clone(),
prepared_artifact: &self.artifact,
prepared_artifact_authority: &self.artifact_authority,
prepared_artifact_label: &self.artifact_label,
root_process: RootProcess::new(
SANDBOX_INIT_DESTINATION,
[PRIVATE_SANDBOX_INIT_ROLE.to_owned()],
)
.map_err(failure)?,
resolver_addresses: self.resolver_addresses.clone(),
network_probe: self.network_probe.clone(),
reachability: self.reachability.clone(),
container_id: self.container_id,
invoking_uid: rustix::process::getuid().as_raw(),
escalation_client: &self.escalation_client,
sandbox_init: &self.sandbox_init,
})
}
}
/// Selects and prepares every Host Coordinator-owned cold-start input.
///
/// # Errors
/// Refuses invalid installed paths, unsupported architectures, registry or image failures,
/// changed Storage Root authority, exhausted identities, and configuration-policy mismatch.
pub async fn select_runtime(
configuration: &ResolvedDomainConfiguration,
store: Arc<HostCoordinatorDomainStore>,
product_paths: InstalledProductPaths<'_>,
) -> Result<RuntimeSelection, RuntimeSelectionError> {
let kernel_release = std::fs::read_to_string("/proc/sys/kernel/osrelease").map_err(failure)?;
select_runtime_for_platform(
configuration,
store,
product_paths,
&PlatformObservation {
operating_system: std::env::consts::OS,
architecture: std::env::consts::ARCH,
kernel_release: kernel_release.trim(),
},
)
.await
}
async fn select_runtime_for_platform(
configuration: &ResolvedDomainConfiguration,
store: Arc<HostCoordinatorDomainStore>,
product_paths: InstalledProductPaths<'_>,
observation: &PlatformObservation<'_>,
) -> Result<RuntimeSelection, RuntimeSelectionError> {
check_platform(observation).map_err(failure)?;
let escalation_client = open_installed_executable(product_paths.escalation_client)?;
let sandbox_init = open_installed_executable(product_paths.sandbox_init)?;
let catalog = Catalog::under_storage_root(configuration.storage_root()).map_err(failure)?;
match catalog.initialize() {
Ok(()) | Err(CatalogError::AlreadyInitialized) => {}
Err(error) => return Err(failure(error)),
}
let prepared =
PreparedLowerStore::under_storage_root(configuration.storage_root(), catalog.clone())
.map_err(failure)?;
recover_before_cold_start(configuration, &store, &catalog, &prepared).map_err(failure)?;
let image = OciRegistryTransport
.pull_confined(
&configuration.fingerprint_record().image,
&RegistryCredentials::Anonymous,
&Platform::linux("amd64"),
configuration.storage_root(),
random_id()?,
)
.await
.map_err(|error| failure(format!("pulling configured anonymous OCI image: {error:#}")))?;
let artifact = prepared
.prepare_transported(
PrepareOperationId::new(random_id()?),
&image,
rootless_id_map(),
prepared_destinations(configuration),
)
.map_err(failure)?;
let artifact_authority = prepared
.open_artifact_authority(&artifact)
.map_err(failure)?;
let artifact_label = PathBuf::from(std::ffi::OsString::from_vec(
configuration
.storage_root()
.canonical_label()
.as_bytes()
.to_vec(),
))
.join("prepared-lower")
.join(PathBuf::from(std::ffi::OsString::from_vec(
artifact.path.as_bytes().to_vec(),
)));
image.cleanup().map_err(failure)?;
let next_generation = next_generation(&store)?;
let (coordinator_id, network_sidecar_id, sandbox_id) = distinct_generation_ids()?;
let generation = Generation {
domain_id: configuration.domain_id(),
generation_id: GenerationId::new(next_generation),
configuration_fingerprint: configuration.configuration_fingerprint(),
coordinator_id,
network_sidecar_id,
sandbox_id,
durable_decision: DurableDecision::None,
};
let namespace = namespace_identity(Path::new(NETWORK_NAMESPACE_PATH)).map_err(failure)?;
let (resolver_addresses, network_probe, reachability) =
select_network_readiness(configuration, &generation, namespace);
Ok(RuntimeSelection {
store,
domain_id: configuration.domain_id(),
configuration_fingerprint: configuration.configuration_fingerprint(),
catalog,
artifact,
artifact_authority,
artifact_label,
generation,
resolver_addresses,
network_probe,
reachability,
container_id: ContainerId::new(random_id()?),
escalation_client,
sandbox_init,
})
}
fn prepared_destinations(configuration: &ResolvedDomainConfiguration) -> Vec<PreparedDestination> {
let mut destinations = [
"/dev",
"/proc",
"/sys",
"/tmp",
"/etc",
"/home",
"/run",
WORKSPACE_DESTINATION,
CREDENTIALS_DESTINATION,
MAILBOX_REQUESTS_DESTINATION,
MAILBOX_RESULTS_DESTINATION,
TERMINFO_ROOT_DESTINATION,
]
.into_iter()
.map(|path| PreparedDestination {
path: path.into(),
kind: PreparedDestinationKind::Directory,
})
.chain(
[
"/etc/resolv.conf",
"/etc/hosts",
"/etc/hostname",
ESCALATION_CLIENT_DESTINATION,
SANDBOX_INIT_DESTINATION,
]
.into_iter()
.map(|path| PreparedDestination {
path: path.into(),
kind: PreparedDestinationKind::RegularFile,
}),
)
.collect::<Vec<_>>();
destinations.extend(
configuration
.fingerprint_record()
.persistent_dirs
.iter()
.map(|path| PreparedDestination {
path: path.into(),
kind: PreparedDestinationKind::Directory,
}),
);
destinations.extend(
configuration
.volumes()
.iter()
.map(|volume| PreparedDestination {
path: volume.record().container_path.as_str().into(),
kind: match volume.record().kind {
VolumeKind::Directory => PreparedDestinationKind::Directory,
VolumeKind::File => PreparedDestinationKind::RegularFile,
},
}),
);
destinations
}
fn next_generation(store: &HostCoordinatorDomainStore) -> Result<u64, RuntimeSelectionError> {
store
.snapshot()
.map_err(failure)?
.generations
.iter()
.map(|generation| generation.generation_id.get())
.max()
.unwrap_or(0)
.checked_add(1)
.ok_or_else(|| failure("generation identity exhausted"))
}
fn select_network_readiness(
configuration: &ResolvedDomainConfiguration,
generation: &Generation,
enforcement_namespace: lazaret_core::identity::NamespaceIdentity,
) -> (Vec<IpAddress>, NetworkProbeRequest, ReachabilityExpectation) {
let readiness = configuration.network_readiness();
let denied = readiness
.denied
.iter()
.map(|target| TcpProbeTarget {
address: IpAddress::V4(target.address),
port: target.port,
})
.collect::<Vec<_>>();
let permitted = readiness
.permitted
.iter()
.map(|target| TcpProbeTarget {
address: IpAddress::V4(target.address),
port: target.port,
})
.collect::<Vec<_>>();
let dns = readiness
.dns
.iter()
.map(|target| DnsProbeTarget {
resolver: IpAddress::V4(target.resolver),
query_name: target.query.clone(),
})
.collect::<Vec<_>>();
let reachability = ReachabilityExpectation {
denied_destinations: denied.iter().map(|target| target.address).collect(),
permitted_destinations: permitted.iter().map(|target| target.address).collect(),
dns_queries: dns
.iter()
.map(|target| DnsQueryExpectation {
resolver: target.resolver,
query_name: target.query_name.clone(),
})
.collect(),
};
let resolvers = dns
.iter()
.map(|target| target.resolver)
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
(
resolvers,
NetworkProbeRequest {
domain_id: generation.domain_id,
generation_id: generation.generation_id,
enforcement_namespace,
denied,
permitted,
dns,
},
reachability,
)
}
fn open_installed_executable(path: &Path) -> Result<File, RuntimeSelectionError> {
let descriptor = openat2(
rustix::fs::CWD,
path,
OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::empty(),
ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_SYMLINKS,
)
.map_err(failure)?;
let file = File::from(descriptor);
let metadata = file.metadata().map_err(failure)?;
if !metadata.is_file() || metadata.permissions().mode() & 0o111 == 0 {
return Err(failure(
"installed product path is not an executable regular file",
));
}
Ok(file)
}
fn rootless_id_map() -> IdMap {
IdMap {
uid: vec![IdMapRange {
container_id: 0,
namespace_id: rustix::process::getuid().as_raw(),
size: 1,
}],
gid: vec![IdMapRange {
container_id: 0,
namespace_id: rustix::process::getgid().as_raw(),
size: 1,
}],
}
}
fn distinct_generation_ids()
-> Result<(CoordinatorId, NetworkSidecarId, SandboxId), RuntimeSelectionError> {
for _ in 0..8 {
let coordinator = CoordinatorId::new(random_id()?);
let network = NetworkSidecarId::new(random_id()?);
let sandbox = SandboxId::new(random_id()?);
if coordinator.as_bytes() != network.as_bytes()
&& coordinator.as_bytes() != sandbox.as_bytes()
&& network.as_bytes() != sandbox.as_bytes()
{
return Ok((coordinator, network, sandbox));
}
}
Err(failure(
"fresh generation role identities repeatedly collided",
))
}
fn random_id() -> Result<[u8; 32], RuntimeSelectionError> {
let mut bytes = [0; 32];
getrandom::fill(&mut bytes).map_err(failure)?;
Ok(bytes)
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use std::fs::{self, OpenOptions};
use std::future::Future;
use std::os::unix::ffi::OsStringExt;
use std::path::PathBuf;
use std::task::{Context, Poll, Waker};
use lazaret_core::configuration::{ConfigurationLoadRequest, load_trusted_configuration};
use lazaret_core::identity::{DeviceId, InodeId, NamespaceIdentity};
use lazaret_core::records::Domain;
use lazaret_linux::filesystem::try_exclusive_flock;
use lazaret_runtime_supervisor::sandbox::default_sandbox_layout;
use crate::domain_store::DomainStoreMutation;
use super::*;
#[test]
fn resolved_readiness_maps_exactly_and_rootless_id_map_matches_builder_policy() {
let tree = tempfile::tempdir().unwrap();
let configuration = configuration(tree.path());
let generation = Generation {
domain_id: configuration.domain_id(),
generation_id: GenerationId::new(9),
configuration_fingerprint: configuration.configuration_fingerprint(),
coordinator_id: CoordinatorId::new([1; 32]),
network_sidecar_id: NetworkSidecarId::new([2; 32]),
sandbox_id: SandboxId::new([3; 32]),
durable_decision: DurableDecision::None,
};
let namespace = NamespaceIdentity {
device: DeviceId::new(4),
inode: InodeId::new(5),
};
let (resolvers, request, expectation) =
select_network_readiness(&configuration, &generation, namespace);
assert_eq!(resolvers, [IpAddress::V4([198, 51, 100, 53])]);
assert_eq!(request.denied[0].port, 8443);
assert_eq!(request.permitted[0].port, 443);
assert_eq!(request.dns[0].query_name, "example.invalid");
assert_eq!(expectation.denied_destinations, [request.denied[0].address]);
assert_eq!(
expectation.permitted_destinations,
[request.permitted[0].address]
);
assert_eq!(expectation.dns_queries[0].resolver, request.dns[0].resolver);
let id_map = rootless_id_map();
assert_eq!(id_map.uid[0].container_id, 0);
assert_eq!(
id_map.uid[0].namespace_id,
rustix::process::getuid().as_raw()
);
assert_eq!(
id_map.gid[0].namespace_id,
rustix::process::getgid().as_raw()
);
let destinations = prepared_destinations(&configuration);
assert!(destinations.iter().any(|destination| {
destination.path == Path::new(WORKSPACE_DESTINATION)
&& destination.kind == PreparedDestinationKind::Directory
}));
assert!(destinations.iter().any(|destination| {
destination.path == Path::new(SANDBOX_INIT_DESTINATION)
&& destination.kind == PreparedDestinationKind::RegularFile
}));
}
#[test]
fn installed_product_selection_requires_exact_executable_and_fresh_roles() {
let tree = tempfile::tempdir().unwrap();
let executable = tree.path().join("product");
fs::copy("/bin/true", &executable).unwrap();
fs::set_permissions(&executable, fs::Permissions::from_mode(0o700)).unwrap();
assert!(open_installed_executable(&executable).is_ok());
fs::set_permissions(&executable, fs::Permissions::from_mode(0o600)).unwrap();
assert!(open_installed_executable(&executable).is_err());
let (coordinator, network, sandbox) = distinct_generation_ids().unwrap();
assert_ne!(coordinator.as_bytes(), network.as_bytes());
assert_ne!(coordinator.as_bytes(), sandbox.as_bytes());
assert_ne!(network.as_bytes(), sandbox.as_bytes());
}
#[test]
fn unsupported_platforms_refuse_before_installed_or_storage_side_effects() {
for observation in [
PlatformObservation {
operating_system: "linux",
architecture: "x86_64",
kernel_release: "6.8.14",
},
PlatformObservation {
operating_system: "linux",
architecture: "aarch64",
kernel_release: "6.9.0",
},
] {
let tree = tempfile::tempdir().unwrap();
let configuration = configuration(tree.path());
let store = domain_store(&configuration);
let mut future = Box::pin(select_runtime_for_platform(
&configuration,
store,
InstalledProductPaths {
escalation_client: Path::new("/missing-escalation-client"),
sandbox_init: Path::new("/missing-sandbox-init"),
},
&observation,
));
let mut context = Context::from_waker(Waker::noop());
let Poll::Ready(result) = future.as_mut().poll(&mut context) else {
panic!("platform refusal performed asynchronous work");
};
assert!(result.is_err());
let storage = tree.path().join("data");
assert!(!storage.join("storage-catalog.lock").exists());
assert!(!storage.join("catalog.redb").exists());
assert!(!storage.join("prepared-lower").exists());
assert!(!storage.join("image-transports").exists());
}
}
#[test]
fn generation_selection_and_cold_start_retain_one_exact_domain_store() {
let tree = tempfile::tempdir().unwrap();
let configuration = configuration(tree.path());
let first_root = tree.path().join("store-a");
let second_root = tree.path().join("store-b");
fs::create_dir(&first_root).unwrap();
fs::create_dir(&second_root).unwrap();
let first = domain_store_at(&configuration, &first_root);
let second = domain_store_at(&configuration, &second_root);
second
.apply(&[DomainStoreMutation::PutGeneration(Generation {
domain_id: configuration.domain_id(),
generation_id: GenerationId::new(1),
configuration_fingerprint: configuration.configuration_fingerprint(),
coordinator_id: CoordinatorId::new([1; 32]),
network_sidecar_id: NetworkSidecarId::new([2; 32]),
sandbox_id: SandboxId::new([3; 32]),
durable_decision: DurableDecision::None,
})])
.unwrap();
assert_eq!(next_generation(&first).unwrap(), 1);
assert_eq!(next_generation(&second).unwrap(), 2);
assert!(!Arc::ptr_eq(&first, &second));
}
fn domain_store(
configuration: &ResolvedDomainConfiguration,
) -> Arc<HostCoordinatorDomainStore> {
let root = PathBuf::from(std::ffi::OsString::from_vec(
configuration
.persistent_domain_root()
.canonical_label()
.as_bytes()
.to_vec(),
));
domain_store_at(configuration, &root)
}
fn domain_store_at(
configuration: &ResolvedDomainConfiguration,
root: &Path,
) -> Arc<HostCoordinatorDomainStore> {
let lock = OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(root.join("domain.lock"))
.unwrap();
Arc::new(
HostCoordinatorDomainStore::open(
try_exclusive_flock(lock).unwrap(),
&root.join("domain.redb"),
&Domain {
domain_id: configuration.domain_id(),
profile_name: configuration.selected_profile().to_owned(),
canonical_workspace: configuration.workspace().canonical_label().clone(),
},
)
.unwrap(),
)
}
fn configuration(root: &Path) -> ResolvedDomainConfiguration {
let configuration_directory = root.join("configuration");
let workspace = root.join("workspace");
let runtime = root.join("runtime");
fs::create_dir(&configuration_directory).unwrap();
fs::create_dir(&workspace).unwrap();
fs::create_dir(&runtime).unwrap();
let configuration_path = configuration_directory.join("lazaret.toml");
fs::write(
&configuration_path,
format!(
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n[profiles.default]\nimage = \"example.invalid/factory:v1\"\n[profiles.default.readiness]\ndenied = [{{ address = \"10.20.30.40\", port = 8443 }}]\npermitted = [{{ address = \"192.0.2.10\", port = 443 }}]\ndns = [{{ resolver = \"198.51.100.53\", query = \"Example.Invalid.\" }}]\n",
root.join("data").display()
),
)
.unwrap();
load_trusted_configuration(ConfigurationLoadRequest {
explicit_configuration_path: Some(configuration_path),
selected_profile: "default".into(),
workspace,
home: root.join("home"),
xdg_config_home: None,
xdg_data_home: None,
xdg_runtime_dir: runtime,
environment: BTreeMap::new(),
sandbox_layout: default_sandbox_layout().unwrap(),
})
.unwrap()
}
}

View file

@ -700,7 +700,7 @@ fn configuration(tree: &TestTree) -> lazaret_core::configuration::ResolvedDomain
),
)
.unwrap();
let layout = SandboxLayoutV1::from_production_layout([
let layout = SandboxLayoutV1::from_protected_destinations([
"/workspace".to_owned(),
"/proc".to_owned(),
"/sys".to_owned(),

View file

@ -270,7 +270,7 @@ mod tests {
};
use lazaret_core::records::{Domain, Generation};
use lazaret_linux::filesystem::try_exclusive_flock;
use lazaret_runtime_supervisor::sandbox::production_sandbox_layout;
use lazaret_runtime_supervisor::sandbox::default_sandbox_layout;
use rustix::fs::{FileType, Mode, XattrFlags, fsetxattr, makedev, mknodat};
use super::*;
@ -491,7 +491,7 @@ mod tests {
xdg_data_home: None,
xdg_runtime_dir: runtime,
environment: BTreeMap::new(),
sandbox_layout: production_sandbox_layout().unwrap(),
sandbox_layout: default_sandbox_layout().unwrap(),
})
.unwrap()
}

View file

@ -544,7 +544,7 @@ fn engine_for(fixture: &Fixture) -> (Arc<MockFactory>, SessionTerminalEngine) {
#[test]
#[allow(clippy::too_many_lines)]
fn production_prompt_adapter_retains_resume_resize_before_authenticated_denial() {
fn prompt_adapter_retains_resume_resize_before_authenticated_denial() {
let generation = GenerationIdentity {
agent_profile: "trusted".into(),
domain_id: DomainId::new(bytes(1)),

View file

@ -436,7 +436,7 @@ mod tests {
use lazaret_core::identity::{CoordinatorId, NetworkSidecarId, SandboxId};
use lazaret_core::records::{Domain, DurableDecision, Generation};
use lazaret_linux::filesystem::try_exclusive_flock;
use lazaret_runtime_supervisor::sandbox::production_sandbox_layout;
use lazaret_runtime_supervisor::sandbox::default_sandbox_layout;
use crate::domain_store::{DomainStoreMutation, HostCoordinatorDomainStore};
use crate::lifecycle::{LifecycleAction, SessionAdmissionIdentity};
@ -757,7 +757,7 @@ mod tests {
xdg_data_home: None,
xdg_runtime_dir: runtime,
environment: BTreeMap::new(),
sandbox_layout: production_sandbox_layout().unwrap(),
sandbox_layout: default_sandbox_layout().unwrap(),
})
.unwrap()
}

Some files were not shown because too many files have changed in this diff Show more