Compose cold-start resources under Runtime Supervisor ownership
This commit is contained in:
parent
510dfccf5a
commit
33ac0ba4ee
7 changed files with 1827 additions and 139 deletions
483
crates/lazaret-host-coordinator/src/bootstrap.rs
Normal file
483
crates/lazaret-host-coordinator/src/bootstrap.rs
Normal file
|
|
@ -0,0 +1,483 @@
|
|||
use std::fmt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use lazaret_core::configuration::ResolvedDomainConfiguration;
|
||||
use lazaret_core::identity::{BundleDigest, ContainerId, MountIdentity};
|
||||
use lazaret_core::network::{CanonicalDerivedNetworkPolicy, ReachabilityExpectation};
|
||||
use lazaret_core::records::Session;
|
||||
use lazaret_core::records::{Generation, GenerationIdentity, PreparedArtifact};
|
||||
use lazaret_linux::process::current_process_identity;
|
||||
use lazaret_runtime_supervisor::network::NetworkProbeRequest;
|
||||
use lazaret_runtime_supervisor::sandbox::SandboxBundle;
|
||||
use lazaret_runtime_supervisor::supervision::RuntimeSupervisorLaunch;
|
||||
use lazaret_state_anchor::StateAnchorLayout;
|
||||
use lazaret_storage_catalog::Catalog;
|
||||
|
||||
use crate::control::ControlSessionAdmission;
|
||||
use crate::control::{PublishedControlFace, SessionRegistry, StagedControlFace, TrustAnchor};
|
||||
use crate::domain_store::HostCoordinatorDomainStore;
|
||||
use crate::lifecycle::{
|
||||
HostCoordinatorLifecycle, LifecycleAction, LifecycleAdapter, SessionAdmissionIdentity,
|
||||
SessionLifecycleAdapter,
|
||||
};
|
||||
use crate::network::NetworkCoordinator;
|
||||
use crate::runtime::{AwaitingAttachedRunsc, CommittedSandboxRuntime, SandboxCoordinator};
|
||||
use crate::state_anchor::{StateAnchorCoordinator, StateAnchorReleaseAuthority};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProductionLifecycleError(String);
|
||||
|
||||
impl fmt::Display for ProductionLifecycleError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ProductionLifecycleError {}
|
||||
|
||||
fn failure(error: impl fmt::Display) -> ProductionLifecycleError {
|
||||
ProductionLifecycleError(error.to_string())
|
||||
}
|
||||
|
||||
pub struct ProductionBootstrapPlan<'a> {
|
||||
pub store: Arc<HostCoordinatorDomainStore>,
|
||||
pub catalog: &'a Catalog,
|
||||
pub configuration: &'a ResolvedDomainConfiguration,
|
||||
pub generation_identity: GenerationIdentity,
|
||||
pub prepared_artifact: &'a PreparedArtifact,
|
||||
pub state_anchor_layout: &'a StateAnchorLayout<'a>,
|
||||
pub runtime_launch: RuntimeSupervisorLaunch,
|
||||
pub sandbox_bundle: &'a SandboxBundle,
|
||||
pub network_policy: &'a CanonicalDerivedNetworkPolicy,
|
||||
pub network_probe: NetworkProbeRequest,
|
||||
pub reachability: &'a ReachabilityExpectation,
|
||||
pub container_id: ContainerId,
|
||||
pub runtime_root: PathBuf,
|
||||
pub bundle_digest: BundleDigest,
|
||||
pub invoking_uid: u32,
|
||||
}
|
||||
|
||||
pub struct ProductionLifecycleAdapter<'a> {
|
||||
plan: ProductionBootstrapPlan<'a>,
|
||||
registry: Arc<SessionRegistry>,
|
||||
trust_anchor: Option<TrustAnchor>,
|
||||
staged_control: Option<StagedControlFace>,
|
||||
published_control: Option<PublishedControlFace>,
|
||||
awaiting_runtime: Option<AwaitingAttachedRunsc>,
|
||||
runtime: Option<CommittedSandboxRuntime>,
|
||||
state_anchor_mount: Option<MountIdentity>,
|
||||
state_anchor_release: Option<StateAnchorReleaseAuthority>,
|
||||
}
|
||||
|
||||
pub type ProductionHostCoordinatorLifecycle<'store, 'resources> = HostCoordinatorLifecycle<
|
||||
'store,
|
||||
ProductionLifecycleAdapter<'resources>,
|
||||
ProductionSessionRegistry,
|
||||
>;
|
||||
|
||||
/// Composes the cold-start lifecycle with the exact shared production session registry.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses a plan whose Domain Store is not the caller-retained lock-owning store.
|
||||
pub fn production_lifecycle<'store, 'resources>(
|
||||
store: &'store Arc<HostCoordinatorDomainStore>,
|
||||
plan: ProductionBootstrapPlan<'resources>,
|
||||
) -> Result<ProductionHostCoordinatorLifecycle<'store, 'resources>, ProductionLifecycleError> {
|
||||
if !Arc::ptr_eq(store, &plan.store) {
|
||||
return Err(ProductionLifecycleError(
|
||||
"production bootstrap Domain Store authority differs".into(),
|
||||
));
|
||||
}
|
||||
let (adapter, registry) = ProductionLifecycleAdapter::new(plan);
|
||||
Ok(HostCoordinatorLifecycle::new(store, adapter, registry))
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProductionSessionRegistry {
|
||||
registry: Arc<SessionRegistry>,
|
||||
}
|
||||
|
||||
impl ProductionSessionRegistry {
|
||||
#[must_use]
|
||||
pub fn registry(&self) -> Arc<SessionRegistry> {
|
||||
Arc::clone(&self.registry)
|
||||
}
|
||||
}
|
||||
|
||||
impl SessionLifecycleAdapter for ProductionSessionRegistry {
|
||||
type Error = ProductionLifecycleError;
|
||||
type Admission = ControlSessionAdmission;
|
||||
|
||||
fn admission_identity(&self, admission: &Self::Admission) -> SessionAdmissionIdentity {
|
||||
<Arc<SessionRegistry> as SessionLifecycleAdapter>::admission_identity(
|
||||
&self.registry,
|
||||
admission,
|
||||
)
|
||||
}
|
||||
|
||||
fn register_session(
|
||||
&self,
|
||||
store: &HostCoordinatorDomainStore,
|
||||
entry_action: LifecycleAction,
|
||||
admission: Self::Admission,
|
||||
) -> Result<Session, Self::Error> {
|
||||
<Arc<SessionRegistry> as SessionLifecycleAdapter>::register_session(
|
||||
&self.registry,
|
||||
store,
|
||||
entry_action,
|
||||
admission,
|
||||
)
|
||||
.map_err(failure)
|
||||
}
|
||||
|
||||
fn disconnect_session(
|
||||
&self,
|
||||
store: &HostCoordinatorDomainStore,
|
||||
session: &Session,
|
||||
) -> Result<(), Self::Error> {
|
||||
<Arc<SessionRegistry> as SessionLifecycleAdapter>::disconnect_session(
|
||||
&self.registry,
|
||||
store,
|
||||
session,
|
||||
)
|
||||
.map_err(failure)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ProductionLifecycleAdapter<'a> {
|
||||
#[must_use]
|
||||
pub fn new(plan: ProductionBootstrapPlan<'a>) -> (Self, ProductionSessionRegistry) {
|
||||
let registry = Arc::new(SessionRegistry::new(
|
||||
Arc::clone(&plan.store),
|
||||
plan.generation_identity.clone(),
|
||||
));
|
||||
(
|
||||
Self {
|
||||
plan,
|
||||
registry: Arc::clone(®istry),
|
||||
trust_anchor: None,
|
||||
staged_control: None,
|
||||
published_control: None,
|
||||
awaiting_runtime: None,
|
||||
runtime: None,
|
||||
state_anchor_mount: None,
|
||||
state_anchor_release: None,
|
||||
},
|
||||
ProductionSessionRegistry { registry },
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn published_control(&self) -> Option<&PublishedControlFace> {
|
||||
self.published_control.as_ref()
|
||||
}
|
||||
|
||||
fn require_generation(&self, generation: &Generation) -> Result<(), ProductionLifecycleError> {
|
||||
if generation.domain_id == self.plan.generation_identity.domain_id
|
||||
&& generation.generation_id == self.plan.generation_identity.generation_id
|
||||
&& generation.configuration_fingerprint
|
||||
== self.plan.generation_identity.configuration_fingerprint
|
||||
&& generation.coordinator_id == self.plan.generation_identity.coordinator_id
|
||||
&& generation.sandbox_id == self.plan.generation_identity.sandbox_id
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ProductionLifecycleError(
|
||||
"production bootstrap generation identity changed".into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn store(&self) -> &HostCoordinatorDomainStore {
|
||||
&self.plan.store
|
||||
}
|
||||
|
||||
fn sandbox(&self) -> SandboxCoordinator<'_> {
|
||||
SandboxCoordinator::new(self.store())
|
||||
}
|
||||
|
||||
fn network(&self) -> NetworkCoordinator<'_> {
|
||||
NetworkCoordinator::new(self.store())
|
||||
}
|
||||
|
||||
fn state_anchor(&self) -> StateAnchorCoordinator<'_> {
|
||||
StateAnchorCoordinator::new(self.store(), self.plan.catalog)
|
||||
}
|
||||
|
||||
fn create_trust_anchor(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
if self.trust_anchor.is_some() {
|
||||
return Err(ProductionLifecycleError(
|
||||
"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> {
|
||||
let sandbox = self.sandbox();
|
||||
sandbox
|
||||
.begin_intent(
|
||||
self.plan.generation_identity.generation_id,
|
||||
self.plan.container_id,
|
||||
&self.plan.runtime_root,
|
||||
self.plan.bundle_digest,
|
||||
current_process_identity().map_err(failure)?,
|
||||
)
|
||||
.map_err(failure)?;
|
||||
let launch = &self.plan.runtime_launch;
|
||||
self.awaiting_runtime = Some(
|
||||
AwaitingAttachedRunsc::launch_networked(&sandbox, &self.network(), launch)
|
||||
.map_err(failure)?,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn lease_prepared_lower(&self) -> Result<(), ProductionLifecycleError> {
|
||||
self.state_anchor()
|
||||
.begin_acquisition(
|
||||
self.plan.generation_identity.generation_id,
|
||||
self.plan.prepared_artifact,
|
||||
self.plan.state_anchor_layout,
|
||||
)
|
||||
.map(|_| ())
|
||||
.map_err(failure)
|
||||
}
|
||||
|
||||
fn mount_state_anchor(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
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()))?
|
||||
.mount_state_anchor(
|
||||
&coordinator,
|
||||
self.plan.prepared_artifact.prepared_artifact_id,
|
||||
)
|
||||
.map_err(failure)?;
|
||||
self.state_anchor_mount = Some(mount);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start_connectivity_helper(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
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()))?
|
||||
.receive_helper_staged(&network)
|
||||
.map(|_| ())
|
||||
.map_err(failure)
|
||||
}
|
||||
|
||||
fn start_sidecar(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
self.awaiting_runtime
|
||||
.as_mut()
|
||||
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?
|
||||
.start_sidecar()
|
||||
.map_err(failure)
|
||||
}
|
||||
|
||||
fn mark_network_ready(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
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()))?
|
||||
.receive_network_readiness(
|
||||
&network,
|
||||
self.plan.network_policy,
|
||||
|namespace| {
|
||||
probe.enforcement_namespace = namespace;
|
||||
probe
|
||||
},
|
||||
self.plan.reachability,
|
||||
)
|
||||
.map(|_| ())
|
||||
.map_err(failure)
|
||||
}
|
||||
|
||||
fn start_sandbox(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
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()))?;
|
||||
let staged = pending.receive_staged(&sandbox).map_err(failure)?;
|
||||
let handoff = self
|
||||
.plan
|
||||
.sandbox_bundle
|
||||
.capture_handoff_authority(self.plan.configuration)
|
||||
.map_err(failure)?;
|
||||
let (runtime, _) = pending
|
||||
.acknowledge(staged, &handoff)
|
||||
.map_err(failure)?
|
||||
.commit_networked(&sandbox)
|
||||
.map_err(failure)?;
|
||||
self.runtime = Some(runtime);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stage_publication(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
let anchor = self
|
||||
.trust_anchor
|
||||
.take()
|
||||
.ok_or_else(|| ProductionLifecycleError("Trust Anchor is absent".into()))?;
|
||||
self.staged_control = Some(
|
||||
StagedControlFace::stage(
|
||||
path_from_host(
|
||||
self.plan
|
||||
.configuration
|
||||
.runtime_domain_root()
|
||||
.canonical_label()
|
||||
.as_bytes(),
|
||||
),
|
||||
self.plan.invoking_uid,
|
||||
anchor,
|
||||
Arc::clone(&self.registry),
|
||||
)
|
||||
.map_err(failure)?,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn expose_publication(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
self.published_control = Some(
|
||||
self.staged_control
|
||||
.take()
|
||||
.ok_or_else(|| ProductionLifecycleError("staged control face is absent".into()))?
|
||||
.expose()
|
||||
.map_err(failure)?,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop_sandbox(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
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()))?
|
||||
.stop_sandbox(&sandbox)
|
||||
.map_err(failure)
|
||||
}
|
||||
|
||||
fn stop_sidecar(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
let store = Arc::clone(&self.plan.store);
|
||||
let network = NetworkCoordinator::new(&store);
|
||||
network
|
||||
.begin_release(self.plan.generation_identity.generation_id)
|
||||
.map_err(failure)?;
|
||||
self.runtime
|
||||
.as_mut()
|
||||
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
|
||||
.stop_sidecar()
|
||||
.map_err(failure)?;
|
||||
network
|
||||
.complete_clean_after_absence(self.plan.generation_identity.generation_id)
|
||||
.map_err(failure)
|
||||
}
|
||||
|
||||
fn unmount_state_anchor(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
let coordinator = self.state_anchor();
|
||||
self.state_anchor_release = Some(
|
||||
coordinator
|
||||
.begin_release(
|
||||
self.plan.generation_identity.generation_id,
|
||||
self.plan.state_anchor_layout,
|
||||
)
|
||||
.map_err(failure)?,
|
||||
);
|
||||
self.runtime
|
||||
.as_mut()
|
||||
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
|
||||
.unmount_state_anchor()
|
||||
.map_err(failure)?;
|
||||
self.state_anchor_mount = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn release_prepared_lower(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
let release = self
|
||||
.state_anchor_release
|
||||
.take()
|
||||
.ok_or_else(|| ProductionLifecycleError("State Anchor release is absent".into()))?;
|
||||
self.state_anchor()
|
||||
.complete_release_after_unmount(&release, self.plan.state_anchor_layout)
|
||||
.map_err(failure)
|
||||
}
|
||||
|
||||
fn graceful_exit(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
self.runtime
|
||||
.take()
|
||||
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
|
||||
.graceful_exit()
|
||||
.map_err(failure)
|
||||
}
|
||||
}
|
||||
|
||||
impl LifecycleAdapter for ProductionLifecycleAdapter<'_> {
|
||||
type Error = ProductionLifecycleError;
|
||||
|
||||
fn execute(
|
||||
&mut self,
|
||||
action: LifecycleAction,
|
||||
generation: &Generation,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.require_generation(generation)?;
|
||||
match action {
|
||||
LifecycleAction::CreateTrustAnchor => self.create_trust_anchor(),
|
||||
LifecycleAction::StartRuntimeSupervisor => self.start_runtime_supervisor(),
|
||||
LifecycleAction::LeasePreparedLower => self.lease_prepared_lower(),
|
||||
LifecycleAction::MountStateAnchor => self.mount_state_anchor(),
|
||||
LifecycleAction::StartSidecar => self.start_sidecar(),
|
||||
LifecycleAction::StartConnectivityHelper => self.start_connectivity_helper(),
|
||||
LifecycleAction::MarkNetworkReady => self.mark_network_ready(),
|
||||
LifecycleAction::StartSandbox => self.start_sandbox(),
|
||||
LifecycleAction::PublishDomain => self.stage_publication(),
|
||||
LifecycleAction::StopSandbox => self.stop_sandbox(),
|
||||
LifecycleAction::StopSidecar => self.stop_sidecar(),
|
||||
LifecycleAction::UnmountStateAnchor => self.unmount_state_anchor(),
|
||||
LifecycleAction::ReleasePreparedLowerLease => self.release_prepared_lower(),
|
||||
LifecycleAction::GracefulSupervisorExit => self.graceful_exit(),
|
||||
LifecycleAction::FinishTeardown => {
|
||||
self.published_control.take();
|
||||
Ok(())
|
||||
}
|
||||
LifecycleAction::StartNewDomain
|
||||
| LifecycleAction::StartReadySession
|
||||
| LifecycleAction::WaitForDomain
|
||||
| LifecycleAction::RestartWaitingDomain
|
||||
| LifecycleAction::ArmRuntimeCausalEdge
|
||||
| LifecycleAction::AdvanceWaitingSession
|
||||
| LifecycleAction::RejectConfigurationMismatch
|
||||
| LifecycleAction::RegisterSession
|
||||
| LifecycleAction::DisconnectSession
|
||||
| LifecycleAction::BeginTeardown => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn after_commit(
|
||||
&mut self,
|
||||
action: LifecycleAction,
|
||||
generation: &Generation,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.require_generation(generation)?;
|
||||
if action == LifecycleAction::PublishDomain {
|
||||
self.expose_publication()
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn path_from_host(bytes: &[u8]) -> &Path {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
Path::new(std::ffi::OsStr::from_bytes(bytes))
|
||||
}
|
||||
|
|
@ -49,6 +49,8 @@ pub const MAX_ENROLLMENT_BYTES: usize = 8_192;
|
|||
const MAX_CONTROL_BYTES: usize = 65_536;
|
||||
const CONTROL_SOCKET_NAME: &str = "control.sock";
|
||||
const PUBLICATION_IDENTITY_NAME: &str = "protocol-identity.postcard";
|
||||
const STAGED_CONTROL_SOCKET_NAME: &str = ".control.sock.staged";
|
||||
const STAGED_PUBLICATION_IDENTITY_NAME: &str = ".protocol-identity.staged";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ControlError {
|
||||
|
|
@ -207,6 +209,11 @@ pub struct TrustAnchor {
|
|||
}
|
||||
|
||||
impl TrustAnchor {
|
||||
#[must_use]
|
||||
pub const fn generation_identity(&self) -> &GenerationIdentity {
|
||||
&self.generation_identity
|
||||
}
|
||||
|
||||
/// Creates one ephemeral generation Trust Anchor with role-separated Ed25519 keys.
|
||||
///
|
||||
/// # Errors
|
||||
|
|
@ -412,7 +419,8 @@ pub struct SessionRegistry {
|
|||
}
|
||||
|
||||
impl SessionRegistry {
|
||||
fn new(
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
store: Arc<HostCoordinatorDomainStore>,
|
||||
generation_identity: GenerationIdentity,
|
||||
) -> Self {
|
||||
|
|
@ -676,6 +684,8 @@ struct ControlPaths {
|
|||
root_identity: FileIdentity,
|
||||
socket: PathBuf,
|
||||
identity: PathBuf,
|
||||
staged_socket: PathBuf,
|
||||
staged_identity: PathBuf,
|
||||
}
|
||||
|
||||
impl ControlPaths {
|
||||
|
|
@ -708,6 +718,8 @@ impl ControlPaths {
|
|||
root_identity,
|
||||
socket: root.join(CONTROL_SOCKET_NAME),
|
||||
identity: root.join(PUBLICATION_IDENTITY_NAME),
|
||||
staged_socket: root.join(STAGED_CONTROL_SOCKET_NAME),
|
||||
staged_identity: root.join(STAGED_PUBLICATION_IDENTITY_NAME),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -725,15 +737,15 @@ impl ControlPaths {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn create_identity(
|
||||
fn create_named_identity(
|
||||
&self,
|
||||
name: &str,
|
||||
bytes: &[u8],
|
||||
uid: u32,
|
||||
) -> Result<(File, FileIdentity), ControlError> {
|
||||
self.validate_root(uid)?;
|
||||
let fd = openat(
|
||||
self.root_file.as_fd(),
|
||||
PUBLICATION_IDENTITY_NAME,
|
||||
name,
|
||||
OFlags::CREATE | OFlags::EXCL | OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||
Mode::RUSR | Mode::WUSR,
|
||||
)
|
||||
|
|
@ -749,10 +761,10 @@ impl ControlPaths {
|
|||
self.root_file
|
||||
.sync_all()
|
||||
.map_err(|_| ControlError::InvalidPublication("directory sync failed"))?;
|
||||
self.validate_identity(&file, identity, uid, bytes.len())
|
||||
self.validate_identity_at(&self.root.join(name), &file, identity, uid, bytes.len())
|
||||
})();
|
||||
if let Err(error) = result {
|
||||
remove_if_identity_matches(&self.identity, identity);
|
||||
remove_if_identity_matches(&self.root.join(name), identity);
|
||||
return Err(error);
|
||||
}
|
||||
Ok((file, identity))
|
||||
|
|
@ -764,10 +776,21 @@ impl ControlPaths {
|
|||
expected: FileIdentity,
|
||||
uid: u32,
|
||||
expected_length: usize,
|
||||
) -> Result<(), ControlError> {
|
||||
self.validate_identity_at(&self.identity, file, expected, uid, expected_length)
|
||||
}
|
||||
|
||||
fn validate_identity_at(
|
||||
&self,
|
||||
path: &Path,
|
||||
file: &File,
|
||||
expected: FileIdentity,
|
||||
uid: u32,
|
||||
expected_length: usize,
|
||||
) -> Result<(), ControlError> {
|
||||
self.validate_root(uid)?;
|
||||
let descriptor = file.metadata()?;
|
||||
let current = std::fs::symlink_metadata(&self.identity)?;
|
||||
let current = std::fs::symlink_metadata(path)?;
|
||||
if FileIdentity::from_metadata(&descriptor) != expected
|
||||
|| FileIdentity::from_metadata(¤t) != expected
|
||||
|| !descriptor.file_type().is_file()
|
||||
|
|
@ -801,6 +824,150 @@ pub struct PublishedControlFace {
|
|||
registry: Arc<SessionRegistry>,
|
||||
}
|
||||
|
||||
pub struct StagedControlFace {
|
||||
listener: Option<std::os::unix::net::UnixListener>,
|
||||
paths: ControlPaths,
|
||||
staged_socket_identity: FileIdentity,
|
||||
identity_file: File,
|
||||
identity_file_identity: FileIdentity,
|
||||
publication_identity: ProtocolPublicationIdentity,
|
||||
publication_bytes: Vec<u8>,
|
||||
uid: u32,
|
||||
anchor: Option<TrustAnchor>,
|
||||
server_config: Arc<ServerConfig>,
|
||||
registry: Arc<SessionRegistry>,
|
||||
}
|
||||
|
||||
impl StagedControlFace {
|
||||
/// Stages an undiscoverable private control face before durable publication.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses existing artifacts, changed paths, ownership/mode errors, or TLS failure.
|
||||
pub fn stage(
|
||||
runtime_domain_root: &Path,
|
||||
uid: u32,
|
||||
anchor: TrustAnchor,
|
||||
registry: Arc<SessionRegistry>,
|
||||
) -> Result<Self, ControlError> {
|
||||
if registry.generation_identity != anchor.generation_identity {
|
||||
return Err(ControlError::StaleGeneration);
|
||||
}
|
||||
let paths = ControlPaths::open(runtime_domain_root, uid)?;
|
||||
if [
|
||||
&paths.socket,
|
||||
&paths.identity,
|
||||
&paths.staged_socket,
|
||||
&paths.staged_identity,
|
||||
]
|
||||
.into_iter()
|
||||
.any(|path| path.try_exists().unwrap_or(true))
|
||||
{
|
||||
return Err(ControlError::InvalidPublication(
|
||||
"publication path already exists",
|
||||
));
|
||||
}
|
||||
let publication_identity = anchor.publication_identity();
|
||||
let publication_bytes = encode(&publication_identity)?;
|
||||
let server_config = Arc::new(anchor.server_config()?);
|
||||
let (identity_file, identity_file_identity) = paths.create_named_identity(
|
||||
STAGED_PUBLICATION_IDENTITY_NAME,
|
||||
&publication_bytes,
|
||||
uid,
|
||||
)?;
|
||||
let listener = std::os::unix::net::UnixListener::bind(&paths.staged_socket)
|
||||
.map_err(|_| ControlError::InvalidPublication("staged control socket bind failed"))?;
|
||||
std::fs::set_permissions(&paths.staged_socket, std::fs::Permissions::from_mode(0o600))?;
|
||||
let socket_metadata = std::fs::symlink_metadata(&paths.staged_socket)?;
|
||||
let staged_socket_identity = FileIdentity::from_metadata(&socket_metadata);
|
||||
if !socket_metadata.file_type().is_socket()
|
||||
|| socket_metadata.uid() != uid
|
||||
|| socket_metadata.mode() & 0o7777 != 0o600
|
||||
{
|
||||
return Err(ControlError::InvalidPublication(
|
||||
"staged control socket ownership or mode is invalid",
|
||||
));
|
||||
}
|
||||
paths.root_file.sync_all()?;
|
||||
Ok(Self {
|
||||
listener: Some(listener),
|
||||
paths,
|
||||
staged_socket_identity,
|
||||
identity_file,
|
||||
identity_file_identity,
|
||||
publication_identity,
|
||||
publication_bytes,
|
||||
uid,
|
||||
anchor: Some(anchor),
|
||||
server_config,
|
||||
registry,
|
||||
})
|
||||
}
|
||||
|
||||
/// Exposes the staged face after the exact generation is durably Published.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses stale durable state, changed staged artifacts, or exposure failure.
|
||||
pub fn expose(mut self) -> Result<PublishedControlFace, ControlError> {
|
||||
self.registry.require_serviceable_generation()?;
|
||||
self.paths.validate_root(self.uid)?;
|
||||
let staged_socket = std::fs::symlink_metadata(&self.paths.staged_socket)?;
|
||||
if FileIdentity::from_metadata(&staged_socket) != self.staged_socket_identity {
|
||||
return Err(ControlError::InvalidPublication(
|
||||
"staged control socket changed",
|
||||
));
|
||||
}
|
||||
self.paths.validate_identity_at(
|
||||
&self.paths.staged_identity,
|
||||
&self.identity_file,
|
||||
self.identity_file_identity,
|
||||
self.uid,
|
||||
self.publication_bytes.len(),
|
||||
)?;
|
||||
std::fs::rename(&self.paths.staged_socket, &self.paths.socket)?;
|
||||
if let Err(error) = std::fs::rename(&self.paths.staged_identity, &self.paths.identity) {
|
||||
remove_if_identity_matches(&self.paths.socket, self.staged_socket_identity);
|
||||
return Err(error.into());
|
||||
}
|
||||
self.paths.root_file.sync_all()?;
|
||||
let socket_identity =
|
||||
FileIdentity::from_metadata(&std::fs::symlink_metadata(&self.paths.socket)?);
|
||||
self.paths.validate_identity(
|
||||
&self.identity_file,
|
||||
self.identity_file_identity,
|
||||
self.uid,
|
||||
self.publication_bytes.len(),
|
||||
)?;
|
||||
let paths = ControlPaths::open(&self.paths.root, self.uid)?;
|
||||
Ok(PublishedControlFace {
|
||||
listener: self
|
||||
.listener
|
||||
.take()
|
||||
.ok_or(ControlError::InvalidPublication(
|
||||
"staged listener is absent",
|
||||
))?,
|
||||
paths,
|
||||
socket_identity,
|
||||
identity_file: self.identity_file.try_clone()?,
|
||||
identity_file_identity: self.identity_file_identity,
|
||||
publication_identity: self.publication_identity.clone(),
|
||||
publication_bytes: self.publication_bytes.clone(),
|
||||
uid: self.uid,
|
||||
anchor: self.anchor.take().ok_or(ControlError::InvalidPublication(
|
||||
"staged Trust Anchor is absent",
|
||||
))?,
|
||||
server_config: Arc::clone(&self.server_config),
|
||||
registry: Arc::clone(&self.registry),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for StagedControlFace {
|
||||
fn drop(&mut self) {
|
||||
remove_if_identity_matches(&self.paths.staged_socket, self.staged_socket_identity);
|
||||
remove_if_identity_matches(&self.paths.staged_identity, self.identity_file_identity);
|
||||
}
|
||||
}
|
||||
|
||||
impl PublishedControlFace {
|
||||
/// Publishes one generation control face in the fixed host-private runtime path.
|
||||
///
|
||||
|
|
@ -813,83 +980,11 @@ impl PublishedControlFace {
|
|||
anchor: TrustAnchor,
|
||||
store: Arc<HostCoordinatorDomainStore>,
|
||||
) -> Result<Self, ControlError> {
|
||||
let paths = ControlPaths::open(runtime_domain_root, uid)?;
|
||||
if paths.socket.try_exists()? || paths.identity.try_exists()? {
|
||||
return Err(ControlError::InvalidPublication(
|
||||
"publication path already exists",
|
||||
));
|
||||
}
|
||||
let registry = Arc::new(SessionRegistry::new(
|
||||
store,
|
||||
anchor.generation_identity.clone(),
|
||||
));
|
||||
registry.require_generation_authority()?;
|
||||
let publication_identity = anchor.publication_identity();
|
||||
let publication_bytes = encode(&publication_identity)?;
|
||||
let server_config = Arc::new(anchor.server_config()?);
|
||||
let (identity_file, identity_file_identity) =
|
||||
paths.create_identity(&publication_bytes, uid)?;
|
||||
let listener = match std::os::unix::net::UnixListener::bind(&paths.socket) {
|
||||
Ok(listener) => listener,
|
||||
Err(error) => {
|
||||
let _ = std::fs::remove_file(&paths.identity);
|
||||
return Err(ControlError::InvalidPublication(
|
||||
if error.kind() == io::ErrorKind::PermissionDenied {
|
||||
"control socket bind was denied"
|
||||
} else {
|
||||
"control socket bind failed"
|
||||
},
|
||||
));
|
||||
}
|
||||
};
|
||||
let finalize = (|| {
|
||||
std::fs::set_permissions(&paths.socket, std::fs::Permissions::from_mode(0o600))
|
||||
.map_err(|_| ControlError::InvalidPublication("control socket chmod failed"))?;
|
||||
let socket_metadata = std::fs::symlink_metadata(&paths.socket)?;
|
||||
if !socket_metadata.file_type().is_socket()
|
||||
|| socket_metadata.uid() != uid
|
||||
|| socket_metadata.mode() & 0o7777 != 0o600
|
||||
{
|
||||
return Err(ControlError::InvalidPublication(
|
||||
"control socket ownership or mode is invalid",
|
||||
));
|
||||
}
|
||||
paths.validate_root(uid)?;
|
||||
Ok(FileIdentity::from_metadata(&socket_metadata))
|
||||
})();
|
||||
let socket_identity = match finalize {
|
||||
Ok(identity) => identity,
|
||||
Err(error) => {
|
||||
drop(listener);
|
||||
let _ = std::fs::remove_file(&paths.socket);
|
||||
let _ = std::fs::remove_file(&paths.identity);
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
if let Err(error) = paths.validate_identity(
|
||||
&identity_file,
|
||||
identity_file_identity,
|
||||
uid,
|
||||
publication_bytes.len(),
|
||||
) {
|
||||
drop(listener);
|
||||
let _ = std::fs::remove_file(&paths.socket);
|
||||
remove_if_identity_matches(&paths.identity, identity_file_identity);
|
||||
return Err(error);
|
||||
}
|
||||
Ok(Self {
|
||||
listener,
|
||||
paths,
|
||||
socket_identity,
|
||||
identity_file,
|
||||
identity_file_identity,
|
||||
publication_identity,
|
||||
publication_bytes,
|
||||
uid,
|
||||
anchor,
|
||||
server_config,
|
||||
registry,
|
||||
})
|
||||
StagedControlFace::stage(runtime_domain_root, uid, anchor, registry)?.expose()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#![doc = "Host-side coordination authority for one Lazaret Domain."]
|
||||
|
||||
pub mod bootstrap;
|
||||
pub mod control;
|
||||
pub mod domain_store;
|
||||
pub mod escalation;
|
||||
|
|
|
|||
|
|
@ -45,6 +45,18 @@ pub trait LifecycleAdapter {
|
|||
action: LifecycleAction,
|
||||
generation: &Generation,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Completes a physical boundary that must follow its durable lifecycle commit.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns the adapter's post-commit publication or cleanup failure.
|
||||
fn after_commit(
|
||||
&mut self,
|
||||
_action: LifecycleAction,
|
||||
_generation: &Generation,
|
||||
) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
|
|
@ -209,6 +221,16 @@ where
|
|||
self.registered.len()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn adapter(&self) -> &A {
|
||||
&self.adapter
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn adapter_mut(&mut self) -> &mut A {
|
||||
&mut self.adapter
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
/// Refuses a non-absent Domain, invalid initial records, or adapter/store failure.
|
||||
pub fn start_new_domain(
|
||||
|
|
@ -251,6 +273,28 @@ where
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Drives one cold-start leader through the complete normal publication boundary.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns the first invalid transition, durable update, or production adapter failure.
|
||||
pub fn bootstrap_new_domain(
|
||||
&mut self,
|
||||
generation: Generation,
|
||||
session: Session,
|
||||
) -> Result<(), LifecycleError<A::Error>> {
|
||||
self.start_new_domain(generation, session)?;
|
||||
self.create_trust_anchor()?;
|
||||
self.start_runtime_supervisor()?;
|
||||
self.lease_prepared_lower()?;
|
||||
self.mount_state_anchor()?;
|
||||
self.start_sidecar()?;
|
||||
self.start_connectivity_helper()?;
|
||||
self.mark_network_ready()?;
|
||||
self.arm_runtime_causal_edge()?;
|
||||
self.start_sandbox()?;
|
||||
self.publish_domain()
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
/// Refuses an unpublished, stale, mismatched, duplicate, or unauthenticated session.
|
||||
pub fn admit_session(
|
||||
|
|
@ -529,7 +573,16 @@ where
|
|||
generation.durable_decision = DurableDecision::Published;
|
||||
self.store
|
||||
.apply(&[DomainStoreMutation::PutGeneration(generation.clone())])?;
|
||||
self.generation = Some(generation);
|
||||
self.generation = Some(generation.clone());
|
||||
if let Err(error) = self
|
||||
.adapter
|
||||
.after_commit(LifecycleAction::PublishDomain, &generation)
|
||||
{
|
||||
self.phase = DomainPhase::Failed;
|
||||
self.bootstrap_owner = None;
|
||||
self.published = false;
|
||||
return Err(LifecycleError::Adapter(error));
|
||||
}
|
||||
self.phase = DomainPhase::Ready;
|
||||
self.bootstrap_owner = None;
|
||||
self.published = true;
|
||||
|
|
|
|||
|
|
@ -368,6 +368,92 @@ impl<'store> NetworkCoordinator<'store> {
|
|||
Ok(evidence)
|
||||
}
|
||||
|
||||
/// Records exact helper and network release intent before RS resource stop.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses noncommitted or incomplete role ownership.
|
||||
pub fn begin_release(
|
||||
&self,
|
||||
generation_id: GenerationId,
|
||||
) -> Result<(), NetworkCoordinatorError> {
|
||||
let network = self.participant(generation_id, ParticipantRole::Network)?;
|
||||
let helper = self.participant(generation_id, ParticipantRole::Helper)?;
|
||||
if network.state() != ParticipantState::Committed
|
||||
|| helper.state() != ParticipantState::Committed
|
||||
{
|
||||
return Err(NetworkCoordinatorError::InvalidState(
|
||||
"network roles are not committed",
|
||||
));
|
||||
}
|
||||
let network_ownership =
|
||||
network
|
||||
.ownership()
|
||||
.cloned()
|
||||
.ok_or(NetworkCoordinatorError::InvalidState(
|
||||
"network ownership is absent",
|
||||
))?;
|
||||
let helper_ownership =
|
||||
helper
|
||||
.ownership()
|
||||
.cloned()
|
||||
.ok_or(NetworkCoordinatorError::InvalidState(
|
||||
"helper ownership is absent",
|
||||
))?;
|
||||
self.domain_store.apply(&[
|
||||
DomainStoreMutation::PutParticipant(
|
||||
Participant::snapshot(
|
||||
generation_id,
|
||||
ParticipantRole::Helper,
|
||||
ParticipantState::ReleaseIntent,
|
||||
Some(helper_ownership),
|
||||
)
|
||||
.map_err(|_| NetworkCoordinatorError::InvalidState("helper release is invalid"))?,
|
||||
),
|
||||
DomainStoreMutation::PutParticipant(
|
||||
Participant::snapshot(
|
||||
generation_id,
|
||||
ParticipantRole::Network,
|
||||
ParticipantState::ReleaseIntent,
|
||||
Some(network_ownership),
|
||||
)
|
||||
.map_err(|_| NetworkCoordinatorError::InvalidState("network release is invalid"))?,
|
||||
),
|
||||
])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cleans helper and network roles only after RS reports their physical absence.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses cleanup without exact release intent.
|
||||
pub fn complete_clean_after_absence(
|
||||
&self,
|
||||
generation_id: GenerationId,
|
||||
) -> Result<(), NetworkCoordinatorError> {
|
||||
for role in [ParticipantRole::Helper, ParticipantRole::Network] {
|
||||
if self.participant(generation_id, role)?.state() != ParticipantState::ReleaseIntent {
|
||||
return Err(NetworkCoordinatorError::InvalidState(
|
||||
"network release intent is absent",
|
||||
));
|
||||
}
|
||||
}
|
||||
let clean = [ParticipantRole::Helper, ParticipantRole::Network].map(|role| {
|
||||
Participant::snapshot(
|
||||
generation_id,
|
||||
role,
|
||||
ParticipantState::Clean,
|
||||
Some(ParticipantOwnership::empty(role)),
|
||||
)
|
||||
.map_err(|_| NetworkCoordinatorError::InvalidState("network Clean is invalid"))
|
||||
});
|
||||
let [helper, network] = clean;
|
||||
self.domain_store.apply(&[
|
||||
DomainStoreMutation::CleanParticipantAfterAbsence(helper?),
|
||||
DomainStoreMutation::CleanParticipantAfterAbsence(network?),
|
||||
])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn require_staged(
|
||||
&self,
|
||||
staged: StagedProcessOwnership,
|
||||
|
|
|
|||
|
|
@ -155,6 +155,60 @@ fn independently_observed_namespace_refuses_attestation_mutation() {
|
|||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn committed_network_roles_release_together_only_after_absence() {
|
||||
let fixture = Fixture::new();
|
||||
let (policy, runtime) = fixture.prepare_roles();
|
||||
let namespace = namespace_identity(std::path::Path::new("/proc/self/ns/net")).unwrap();
|
||||
let attestation = attestation(&fixture.generation, policy.digest(), namespace, runtime);
|
||||
let probe = NetworkReachabilityProbe {
|
||||
domain_id: fixture.generation.domain_id,
|
||||
generation_id: fixture.generation.generation_id,
|
||||
enforcement_namespace: namespace,
|
||||
denied_destinations: vec![DeniedDestinationObservation {
|
||||
destination: IpAddress::V4([10, 1, 2, 3]),
|
||||
unreachable: true,
|
||||
}],
|
||||
permitted_destinations: vec![PermittedDestinationObservation {
|
||||
destination: IpAddress::V4([1, 1, 1, 1]),
|
||||
reachable: true,
|
||||
}],
|
||||
dns_observations: vec![DnsObservation {
|
||||
resolver: IpAddress::V4([8, 8, 8, 8]),
|
||||
query_name: "example.com".into(),
|
||||
resolved_addresses: vec![IpAddress::V4([93, 184, 216, 34])],
|
||||
}],
|
||||
};
|
||||
let coordinator = NetworkCoordinator::new(&fixture.store);
|
||||
coordinator
|
||||
.commit_readiness(&policy, attestation, probe, &reachability())
|
||||
.unwrap();
|
||||
assert!(
|
||||
coordinator
|
||||
.complete_clean_after_absence(fixture.generation.generation_id)
|
||||
.is_err()
|
||||
);
|
||||
coordinator
|
||||
.begin_release(fixture.generation.generation_id)
|
||||
.unwrap();
|
||||
coordinator
|
||||
.complete_clean_after_absence(fixture.generation.generation_id)
|
||||
.unwrap();
|
||||
|
||||
let participants = fixture.store.snapshot().unwrap().participants;
|
||||
for role in [ParticipantRole::Helper, ParticipantRole::Network] {
|
||||
let participant = participants
|
||||
.iter()
|
||||
.find(|participant| participant.role() == role)
|
||||
.unwrap();
|
||||
assert_eq!(participant.state(), ParticipantState::Clean);
|
||||
assert_eq!(
|
||||
participant.ownership(),
|
||||
Some(&ParticipantOwnership::empty(role))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn policy() -> CanonicalDerivedNetworkPolicy {
|
||||
derive_policy(DerivedNetworkPolicyFields {
|
||||
address_families: vec![
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ use std::os::unix::ffi::OsStrExt;
|
|||
use std::path::Path;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::state_anchor::StateAnchorCoordinator;
|
||||
use lazaret_core::identity::{
|
||||
BundleDigest, ContainerId, GenerationId, HostPath, NamespaceIdentity, PidfsProcessIdentityV1,
|
||||
BundleDigest, ContainerId, GenerationId, HostPath, MountIdentity, NamespaceIdentity,
|
||||
PidfsProcessIdentityV1, PreparedArtifactId,
|
||||
};
|
||||
use lazaret_core::network::{CanonicalDerivedNetworkPolicy, ReachabilityExpectation};
|
||||
use lazaret_core::records::{
|
||||
|
|
@ -375,6 +377,88 @@ impl AwaitingAttachedRunsc {
|
|||
self.supervisor.as_ref().map(RetainedProcess::identity)
|
||||
}
|
||||
|
||||
/// Requests RS-owned State Anchor acquisition and commits exact staged and mount evidence.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses missing intent, event mismatch, changed ownership, or channel failure.
|
||||
pub fn mount_state_anchor(
|
||||
&mut self,
|
||||
coordinator: &StateAnchorCoordinator<'_>,
|
||||
prepared_artifact_id: PreparedArtifactId,
|
||||
) -> Result<MountIdentity, SandboxCoordinatorError> {
|
||||
let control = self
|
||||
.control
|
||||
.as_ref()
|
||||
.ok_or(SandboxCoordinatorError::InvalidState(
|
||||
"Runtime Supervisor control channel is absent",
|
||||
))?;
|
||||
control.mount_state_anchor()?;
|
||||
let staged = match control.receive()? {
|
||||
SupervisorEvent::StateAnchorStaged(staged) => staged,
|
||||
SupervisorEvent::Refused(detail) => {
|
||||
return Err(SandboxCoordinatorError::Io(io::Error::other(detail)));
|
||||
}
|
||||
_ => {
|
||||
return Err(SandboxCoordinatorError::InvalidState(
|
||||
"Runtime Supervisor did not stage the State Anchor daemon",
|
||||
));
|
||||
}
|
||||
};
|
||||
coordinator
|
||||
.stage_daemon(staged)
|
||||
.map_err(|error| SandboxCoordinatorError::Io(io::Error::other(error)))?;
|
||||
let acknowledgment = coordinator
|
||||
.acknowledge_staged_daemon(staged)
|
||||
.map_err(|error| SandboxCoordinatorError::Io(io::Error::other(error)))?;
|
||||
control.acknowledge_state_anchor(acknowledgment)?;
|
||||
let (ready_staged, mount) = match control.receive()? {
|
||||
SupervisorEvent::StateAnchorReady { staged, mount } => (staged, mount),
|
||||
SupervisorEvent::Refused(detail) => {
|
||||
return Err(SandboxCoordinatorError::Io(io::Error::other(detail)));
|
||||
}
|
||||
_ => {
|
||||
return Err(SandboxCoordinatorError::InvalidState(
|
||||
"Runtime Supervisor did not report State Anchor readiness",
|
||||
));
|
||||
}
|
||||
};
|
||||
if ready_staged != staged {
|
||||
return Err(SandboxCoordinatorError::InvalidState(
|
||||
"State Anchor readiness changed daemon ownership",
|
||||
));
|
||||
}
|
||||
let readiness =
|
||||
lazaret_state_anchor::VerifiedStateAnchorReadiness::from_parts(ready_staged, mount);
|
||||
coordinator
|
||||
.commit_daemon_ready(readiness)
|
||||
.and_then(|()| {
|
||||
coordinator.commit_prepared(staged.generation_id(), prepared_artifact_id, mount)
|
||||
})
|
||||
.map_err(|error| SandboxCoordinatorError::Io(io::Error::other(error)))?;
|
||||
Ok(mount)
|
||||
}
|
||||
|
||||
/// Releases the RS-owned sidecar install path after exact State Anchor readiness.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses an unexpected RS response or channel failure.
|
||||
pub fn start_sidecar(&mut self) -> Result<(), SandboxCoordinatorError> {
|
||||
let control = self
|
||||
.control
|
||||
.as_ref()
|
||||
.ok_or(SandboxCoordinatorError::InvalidState(
|
||||
"Runtime Supervisor control channel is absent",
|
||||
))?;
|
||||
control.start_sidecar()?;
|
||||
if control.receive()? == SupervisorEvent::SidecarStarted {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SandboxCoordinatorError::InvalidState(
|
||||
"Runtime Supervisor did not start the sidecar path",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Launches the RS through the HC broker and commits its staged ownership before execution.
|
||||
///
|
||||
/// # Errors
|
||||
|
|
@ -827,6 +911,70 @@ impl CommittedSandboxRuntime {
|
|||
}
|
||||
}
|
||||
|
||||
/// Stops and durably cleans only the exact attached sandbox runtime.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses missing release intent, unexpected RS response, or exact cleanup failure.
|
||||
pub fn stop_sandbox(
|
||||
&mut self,
|
||||
coordinator: &SandboxCoordinator<'_>,
|
||||
) -> Result<(), SandboxCoordinatorError> {
|
||||
coordinator.begin_release(self.generation_id)?;
|
||||
self.control.stop_sandbox()?;
|
||||
if self.control.receive()? != SupervisorEvent::SandboxStopped {
|
||||
return Err(SandboxCoordinatorError::InvalidState(
|
||||
"Runtime Supervisor did not stop the sandbox",
|
||||
));
|
||||
}
|
||||
coordinator.complete_clean_after_absence(self.generation_id)
|
||||
}
|
||||
|
||||
/// Stops only the RS-owned network resources after sandbox absence.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses an unexpected RS response or channel failure.
|
||||
pub fn stop_sidecar(&mut self) -> Result<(), SandboxCoordinatorError> {
|
||||
self.control.stop_sidecar()?;
|
||||
if self.control.receive()? == SupervisorEvent::SidecarStopped {
|
||||
self.network_evidence = None;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SandboxCoordinatorError::InvalidState(
|
||||
"Runtime Supervisor did not stop the sidecar",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Requests exact RS-owned State Anchor unmount after dependent runtime resources stop.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses an unexpected RS response or channel failure.
|
||||
pub fn unmount_state_anchor(&mut self) -> Result<(), SandboxCoordinatorError> {
|
||||
self.control.unmount_state_anchor()?;
|
||||
if self.control.receive()? == SupervisorEvent::StateAnchorUnmounted {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SandboxCoordinatorError::InvalidState(
|
||||
"Runtime Supervisor did not unmount the State Anchor",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Gracefully exits and reaps the resource-empty exact Runtime Supervisor.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses an unexpected response or an RS that does not exit.
|
||||
pub fn graceful_exit(mut self) -> Result<(), SandboxCoordinatorError> {
|
||||
self.control.exit()?;
|
||||
if self.control.receive()? != SupervisorEvent::Exited {
|
||||
return Err(SandboxCoordinatorError::InvalidState(
|
||||
"Runtime Supervisor did not acknowledge graceful exit",
|
||||
));
|
||||
}
|
||||
self.lifetime.take();
|
||||
wait_and_reap_supervisor(&mut self.supervisor)
|
||||
}
|
||||
|
||||
/// Waits for an exact attached-runsc loss report and reaps the retained RS.
|
||||
///
|
||||
/// # Errors
|
||||
|
|
|
|||
Loading…
Reference in a new issue